From d28a53a6cfab17ca396615eb00979a80f27f9705 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sat, 20 May 2023 12:39:07 +0200 Subject: [PATCH] webdav: fix caching with external auth/plugins Signed-off-by: Nicola Murino --- README.md | 2 +- go.mod | 6 +- go.sum | 10 +-- internal/dataprovider/cacheduser.go | 25 +++++--- internal/dataprovider/dataprovider.go | 88 ++++++++++++++++++--------- internal/dataprovider/scheduler.go | 2 +- internal/dataprovider/user.go | 6 +- internal/webdavd/internal_test.go | 7 +++ internal/webdavd/server.go | 20 ++++-- internal/webdavd/webdavd.go | 7 +++ internal/webdavd/webdavd_test.go | 68 ++++++++++++++++++--- 11 files changed, 181 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index f8b26bb2..cb5d0e8e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ If you report an invalid issue or ask for step-by-step support, your issue will - Per-user authentication methods. - [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps. - Simplified user administrations using [groups](./docs/groups.md). -- [Roles](./docs/roles.md) allow you to create limited administrators who can only create and manage users with their role. +- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role. - Custom authentication via [external programs/HTTP API](./docs/external-auth.md). - Web Client and Web Admin user interfaces support [OpenID Connect](https://openid.net/connect/) authentication and so they can be integrated with identity providers such as [Keycloak](https://www.keycloak.org/). You can find more details [here](./docs/oidc.md). - [Data At Rest Encryption](./docs/dare.md). diff --git a/go.mod b/go.mod index 07d62b70..819d16c2 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/spf13/afero v1.9.5 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.3 github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 github.com/subosito/gotenv v1.4.2 github.com/unrolled/secure v1.13.0 @@ -113,7 +113,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/s2a-go v0.1.3 // indirect + github.com/google/s2a-go v0.1.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.8.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -153,7 +153,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/text v0.9.0 // indirect diff --git a/go.sum b/go.sum index dde83e52..f1d303d4 100644 --- a/go.sum +++ b/go.sum @@ -1199,8 +1199,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20220318212150-b2ab0324ddda/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE= -github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= @@ -1929,8 +1929,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 h1:VsBj3UD2xyAOu7kJw6O/2jjG2UXLFoBzihqDU9Ofg9M= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -1997,8 +1998,9 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= diff --git a/internal/dataprovider/cacheduser.go b/internal/dataprovider/cacheduser.go index 5ad93f01..7ff07f08 100644 --- a/internal/dataprovider/cacheduser.go +++ b/internal/dataprovider/cacheduser.go @@ -77,7 +77,7 @@ func (cache *usersCache) updateLastLogin(username string) { // swapWebDAVUser updates an existing cached user with the specified one // preserving the lock fs if possible // FIXME: this could be racy in rare cases -func (cache *usersCache) swap(userRef *User) { +func (cache *usersCache) swap(userRef *User, plainPassword string) { user := userRef.getACopy() err := user.LoadAndApplyGroupSettings() @@ -85,19 +85,23 @@ func (cache *usersCache) swap(userRef *User) { defer cache.Unlock() if cachedUser, ok := cache.users[user.Username]; ok { - if cachedUser.User.Password != user.Password { - providerLog(logger.LevelDebug, "current password different from the cached one for user %q, removing from cache", - user.Username) - // the password changed, the cached user is no longer valid - delete(cache.users, user.Username) - return - } if err != nil { providerLog(logger.LevelDebug, "unable to load group settings, for user %q, removing from cache, err :%v", user.Username, err) delete(cache.users, user.Username) return } + if plainPassword != "" { + cachedUser.Password = plainPassword + } else { + if cachedUser.User.Password != user.Password { + providerLog(logger.LevelDebug, "current password different from the cached one for user %q, removing from cache", + user.Username) + // the password changed, the cached user is no longer valid + delete(cache.users, user.Username) + return + } + } if cachedUser.User.isFsEqual(&user) { // the updated user has the same fs as the cached one, we can preserve the lock filesystem providerLog(logger.LevelDebug, "current password and fs unchanged for for user %q, swap cached one", @@ -154,7 +158,10 @@ func (cache *usersCache) get(username string) (*CachedUser, bool) { defer cache.RUnlock() cachedUser, ok := cache.users[username] - return &cachedUser, ok + if !ok { + return nil, false + } + return &cachedUser, true } // CacheWebDAVUser add a user to the WebDAV cache diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 7f8eef2f..cef97c18 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -1103,38 +1103,47 @@ func CheckAdminAndPass(username, password, ip string) (Admin, error) { } // CheckCachedUserCredentials checks the credentials for a cached user -func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protocol string, tlsCert *x509.Certificate) error { +func CheckCachedUserCredentials(user *CachedUser, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (*CachedUser, *User, error) { + if !user.User.skipExternalAuth() && isExternalAuthConfigured(loginMethod) { + u, _, err := CheckCompositeCredentials(user.User.Username, password, ip, loginMethod, protocol, tlsCert) + if err != nil { + return nil, nil, err + } + webDAVUsersCache.swap(&u, password) + cu, _ := webDAVUsersCache.get(u.Username) + return cu, &u, nil + } if err := user.User.CheckLoginConditions(); err != nil { - return err + return user, nil, err } if loginMethod == LoginMethodPassword && user.User.Filters.IsAnonymous { - return nil + return user, nil, nil } if loginMethod != LoginMethodPassword { _, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert) if err != nil { - return err + return user, nil, err } if loginMethod == LoginMethodTLSCertificate { if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) { - return fmt.Errorf("certificate login method is not allowed for user %q", user.User.Username) + return user, nil, fmt.Errorf("certificate login method is not allowed for user %q", user.User.Username) } - return nil + return user, nil, nil } } if password == "" { - return ErrInvalidCredentials + return user, nil, ErrInvalidCredentials } if user.Password != "" { if password == user.Password { - return nil + return user, nil, nil } } else { if ok, _ := isPasswordOK(&user.User, password); ok { - return nil + return user, nil, nil } } - return ErrInvalidCredentials + return user, nil, ErrInvalidCredentials } // CheckCompositeCredentials checks multiple credentials. @@ -1689,7 +1698,7 @@ func DeleteRole(name string, executor, ipAddress, executorRole string) error { provider.setUpdatedAt(user) u, err := provider.userExists(user, "") if err == nil { - webDAVUsersCache.swap(&u) + webDAVUsersCache.swap(&u, "") executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u) } } @@ -1721,7 +1730,7 @@ func UpdateGroup(group *Group, users []string, executor, ipAddress, role string) provider.setUpdatedAt(user) u, err := provider.userExists(user, "") if err == nil { - webDAVUsersCache.swap(&u) + webDAVUsersCache.swap(&u, "") } else { RemoveCachedWebDAVUser(user) } @@ -2073,7 +2082,7 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er if err := provider.updateUser(&user); err != nil { return err } - webDAVUsersCache.swap(&user) + webDAVUsersCache.swap(&user, plainPwd) executeAction(operationUpdate, executor, ipAddress, actionObjectUser, username, role, &user) return nil } @@ -2085,7 +2094,7 @@ func UpdateUser(user *User, executor, ipAddress, role string) error { } err := provider.updateUser(user) if err == nil { - webDAVUsersCache.swap(user) + webDAVUsersCache.swap(user, "") executeAction(operationUpdate, executor, ipAddress, actionObjectUser, user.Username, role, user) } return err @@ -2252,7 +2261,7 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, groups []string provider.setUpdatedAt(user) u, err := provider.userExists(user, "") if err == nil { - webDAVUsersCache.swap(&u) + webDAVUsersCache.swap(&u, "") executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u) } else { RemoveCachedWebDAVUser(user) @@ -3950,7 +3959,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi u.Filters.RecoveryCodes = recoveryCodes err = provider.updateUser(&u) if err == nil { - webDAVUsersCache.swap(&u) + webDAVUsersCache.swap(&u, "") } } if err != nil { @@ -4123,11 +4132,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv return user, err } - if mergedUser.Filters.Hooks.ExternalAuthDisabled { - return u, nil - } - - if mergedUser.isExternalAuthCached() { + if mergedUser.skipExternalAuth() { return u, nil } @@ -4184,7 +4189,9 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv user.Filters.RecoveryCodes = u.Filters.RecoveryCodes err = provider.updateUser(&user) if err == nil { - webDAVUsersCache.swap(&user) + if protocol != protocolWebDAV { + webDAVUsersCache.swap(&user, password) + } cachedUserPasswords.Add(user.Username, password, user.Password) } return user, err @@ -4206,11 +4213,7 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string, return user, err } - if mergedUser.Filters.Hooks.ExternalAuthDisabled { - return u, nil - } - - if mergedUser.isExternalAuthCached() { + if mergedUser.skipExternalAuth() { return u, nil } @@ -4257,7 +4260,9 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string, user.Filters.RecoveryCodes = u.Filters.RecoveryCodes err = provider.updateUser(&user) if err == nil { - webDAVUsersCache.swap(&user) + if protocol != protocolWebDAV { + webDAVUsersCache.swap(&user, password) + } cachedUserPasswords.Add(user.Username, password, user.Password) } return user, err @@ -4313,6 +4318,33 @@ func isLastActivityRecent(lastActivity int64, minDelay time.Duration) bool { return diff < minDelay } +func isExternalAuthConfigured(loginMethod string) bool { + if config.ExternalAuthHook != "" { + if config.ExternalAuthScope == 0 { + return true + } + switch loginMethod { + case LoginMethodPassword: + return config.ExternalAuthScope&1 != 0 + case LoginMethodTLSCertificate: + return config.ExternalAuthScope&8 != 0 + case LoginMethodTLSCertificateAndPwd: + return config.ExternalAuthScope&1 != 0 || config.ExternalAuthScope&8 != 0 + } + } + switch loginMethod { + case LoginMethodPassword: + return plugin.Handler.HasAuthScope(plugin.AuthScopePassword) + case LoginMethodTLSCertificate: + return plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) + case LoginMethodTLSCertificateAndPwd: + return plugin.Handler.HasAuthScope(plugin.AuthScopePassword) || + plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) + default: + return false + } +} + func getConfigPath(name, configDir string) string { if !util.IsFileInputValid(name) { return "" diff --git a/internal/dataprovider/scheduler.go b/internal/dataprovider/scheduler.go index 70603861..c7ea4d21 100644 --- a/internal/dataprovider/scheduler.go +++ b/internal/dataprovider/scheduler.go @@ -130,7 +130,7 @@ func checkUserCache() { cachedUserPasswords.Remove(user.Username) delayedQuotaUpdater.resetUserQuota(user.Username) } else { - webDAVUsersCache.swap(&user) + webDAVUsersCache.swap(&user, "") } } lastUserCacheUpdate.Store(checkTime) diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go index 5d53e38f..5cc7b1a5 100644 --- a/internal/dataprovider/user.go +++ b/internal/dataprovider/user.go @@ -1039,14 +1039,16 @@ func (u *User) CanManageMFA() bool { return len(mfa.GetAvailableTOTPConfigs()) > 0 } -func (u *User) isExternalAuthCached() bool { +func (u *User) skipExternalAuth() bool { + if u.Filters.Hooks.ExternalAuthDisabled { + return true + } if u.ID <= 0 { return false } if u.Filters.ExternalAuthCacheTime <= 0 { return false } - return isLastActivityRecent(u.LastLogin, time.Duration(u.Filters.ExternalAuthCacheTime)*time.Second) } diff --git a/internal/webdavd/internal_test.go b/internal/webdavd/internal_test.go index dbd414c6..7b6670f9 100644 --- a/internal/webdavd/internal_test.go +++ b/internal/webdavd/internal_test.go @@ -1694,3 +1694,10 @@ func TestConfigsFromProvider(t *testing.T) { err = dataprovider.UpdateConfigs(nil, "", "", "") assert.NoError(t, err) } + +func TestGetCacheExpirationTime(t *testing.T) { + c := UsersCacheConfig{} + assert.True(t, c.getExpirationTime().IsZero()) + c.ExpirationTime = 1 + assert.False(t, c.getExpirationTime().IsZero()) +} diff --git a/internal/webdavd/server.go b/internal/webdavd/server.go index ed424474..1d495598 100644 --- a/internal/webdavd/server.go +++ b/internal/webdavd/server.go @@ -299,8 +299,20 @@ func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.Us tlsCert = nil loginMethod = dataprovider.LoginMethodPassword } - if err := dataprovider.CheckCachedUserCredentials(cachedUser, password, loginMethod, common.ProtocolWebDAV, tlsCert); err == nil { - return cachedUser.User, true, cachedUser.LockSystem, loginMethod, nil + cu, u, err := dataprovider.CheckCachedUserCredentials(cachedUser, password, ip, loginMethod, common.ProtocolWebDAV, tlsCert) + if err == nil { + if cu != nil { + return cu.User, true, cu.LockSystem, loginMethod, nil + } + lockSystem := webdav.NewMemLS() + cachedUser = &dataprovider.CachedUser{ + User: *u, + Password: password, + LockSystem: lockSystem, + Expiration: s.config.Cache.Users.getExpirationTime(), + } + dataprovider.CacheWebDAVUser(cachedUser) + return cachedUser.User, false, cachedUser.LockSystem, loginMethod, nil } updateLoginMetrics(&cachedUser.User, ip, loginMethod, dataprovider.ErrInvalidCredentials) return user, false, nil, loginMethod, dataprovider.ErrInvalidCredentials @@ -318,9 +330,7 @@ func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.Us User: user, Password: password, LockSystem: lockSystem, - } - if s.config.Cache.Users.ExpirationTime > 0 { - cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute) + Expiration: s.config.Cache.Users.getExpirationTime(), } dataprovider.CacheWebDAVUser(cachedUser) return user, false, lockSystem, loginMethod, nil diff --git a/internal/webdavd/webdavd.go b/internal/webdavd/webdavd.go index cfc081d1..ed08c498 100644 --- a/internal/webdavd/webdavd.go +++ b/internal/webdavd/webdavd.go @@ -85,6 +85,13 @@ type UsersCacheConfig struct { MaxSize int `json:"max_size" mapstructure:"max_size"` } +func (c *UsersCacheConfig) getExpirationTime() time.Time { + if c.ExpirationTime > 0 { + return time.Now().Add(time.Duration(c.ExpirationTime) * time.Minute) + } + return time.Time{} +} + // MimeCacheConfig defines the cache configuration for mime types type MimeCacheConfig struct { Enabled bool `json:"enabled" mapstructure:"enabled"` diff --git a/internal/webdavd/webdavd_test.go b/internal/webdavd/webdavd_test.go index 699f5de3..2607d4b9 100644 --- a/internal/webdavd/webdavd_test.go +++ b/internal/webdavd/webdavd_test.go @@ -1186,7 +1186,7 @@ func TestLoginExternalAuth(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -1215,6 +1215,56 @@ func TestLoginExternalAuth(t *testing.T) { assert.NoError(t, err) } +func TestExternalAuthPasswordChange(t *testing.T) { + if runtime.GOOS == osWindows { + t.Skip("this test is not available on Windows") + } + u := getTestUser() + err := dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, defaultPassword), os.ModePerm) + assert.NoError(t, err) + providerConf.ExternalAuthHook = extAuthPath + providerConf.ExternalAuthScope = 0 + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + client := getWebDavClient(u, false, nil) + assert.NoError(t, checkBasicFunc(client)) + u.Username = defaultUsername + "1" + client = getWebDavClient(u, false, nil) + assert.Error(t, checkBasicFunc(client)) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, defaultPassword+"1"), os.ModePerm) + assert.NoError(t, err) + client = getWebDavClient(u, false, nil) + assert.Error(t, checkBasicFunc(client)) + u.Password = defaultPassword + "1" + client = getWebDavClient(u, false, nil) + assert.NoError(t, checkBasicFunc(client)) + user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, defaultUsername, user.Username) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) + user, _, err = httpdtest.GetUserByUsername(defaultUsername+"1", http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf = config.GetProviderConf() + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + err = os.Remove(extAuthPath) + assert.NoError(t, err) +} + func TestExternalAuthReturningAnonymousUser(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") @@ -1228,7 +1278,7 @@ func TestExternalAuthReturningAnonymousUser(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -1320,7 +1370,7 @@ func TestExternalAuthAnonymousGroupInheritance(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -2880,7 +2930,7 @@ func TestExternatAuthWithClientCert(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm) + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, ""), os.ModePerm) assert.NoError(t, err) providerConf.ExternalAuthHook = extAuthPath providerConf.ExternalAuthScope = 0 @@ -3371,11 +3421,15 @@ func getEncryptedFileSize(size int64) (int64, error) { return int64(encSize) + 33, err } -func getExtAuthScriptContent(user dataprovider.User) []byte { +func getExtAuthScriptContent(user dataprovider.User, password string) []byte { extAuthContent := []byte("#!/bin/sh\n\n") - extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...) + if password != "" { + extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%s\" -a \"$SFTPGO_AUTHD_PASSWORD\" = \"%s\"; then\n", user.Username, password))...) + } else { + extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%s\"; then\n", user.Username))...) + } u, _ := json.Marshal(user) - extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("echo '%v'\n", string(u)))...) + extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("echo '%s'\n", string(u)))...) extAuthContent = append(extAuthContent, []byte("else\n")...) extAuthContent = append(extAuthContent, []byte("echo '{\"username\":\"\"}'\n")...) extAuthContent = append(extAuthContent, []byte("fi\n")...)