add support for anonymous users

Fixes #935

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-07-27 18:54:25 +02:00
parent e3c5cf981f
commit ef0a3bc571
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
18 changed files with 723 additions and 52 deletions

View file

@ -14,7 +14,7 @@ The following settings are inherited from the primary group:
- home dir, if set for the group will replace the one defined for the user. The `%username%` placeholder is replaced with the username
- filesystem config, if the provider set for the group is different from the "local provider" will replace the one defined for the user. The `%username%` placeholder is replaced with the username within the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`
- TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication: if they are not set for the user they are replaced with the value set for the group
- TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication, anonymous user: if they are not set for the user they are replaced with the value set for the group
- starting directory, if the user does not have a starting directory set, the value set for the group is used, if any. The `%username%` placeholder is replaced with the username
The following settings are inherited from the primary and secondary groups:

10
go.mod
View file

@ -51,7 +51,7 @@ require (
github.com/rs/cors v1.8.2
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.27.0
github.com/sftpgo/sdk v0.1.2-0.20220726072922-52d01129ff42
github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a
github.com/shirou/gopsutil/v3 v3.22.6
github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0
@ -66,11 +66,11 @@ require (
go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.25.0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220725212005-46097bf591d3
golang.org/x/net v0.0.0-20220726230323-06994584191e
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/api v0.88.0
google.golang.org/api v0.89.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@ -167,5 +167,5 @@ replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e
golang.org/x/net => github.com/drakkan/net v0.0.0-20220723143609-9fc59277ebad
golang.org/x/net => github.com/drakkan/net v0.0.0-20220727071746-ba26829f1764
)

15
go.sum
View file

@ -263,8 +263,8 @@ github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e h1:ZvOJ5DqEUZig5lGl
github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/net v0.0.0-20220723143609-9fc59277ebad h1:XmHFuEk+opBx+sd+g7sZp0cpBFocU/pf+zTSE+usbrc=
github.com/drakkan/net v0.0.0-20220723143609-9fc59277ebad/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
github.com/drakkan/net v0.0.0-20220727071746-ba26829f1764 h1:54eBbhCnw67BW8q0rlzEjY7Lmpirh337R45gScE2Lfg=
github.com/drakkan/net v0.0.0-20220727071746-ba26829f1764/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM=
github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
@ -708,8 +708,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.2-0.20220726072922-52d01129ff42 h1:I3ecUuSF9i2w/u71x1au13Frh9t30OpprXBnuozMcf4=
github.com/sftpgo/sdk v0.1.2-0.20220726072922-52d01129ff42/go.mod h1:JB0ULmxlNNVe77TQFEULePqQzwCwD5DUmSn+lvsZqp0=
github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a h1:X9qPZ+GPQ87TnBDNZN6dyX7FkjhwnFh98WgB6Y1T5O8=
github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a/go.mod h1:RL4HeorXC6XgqtkLYnQUSogLdsdMfbsogIvdBVLuy4w=
github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ=
github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -971,8 +971,9 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 h1:dyU22nBWzrmTQxtNrr4dzVOvaw35nUYE279vF9UmsI8=
golang.org/x/sys v0.0.0-20220727055044-e65921a090b8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1115,8 +1116,8 @@ google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.88.0 h1:MPwxQRqpyskYhr2iNyfsQ8R06eeyhe7UEuR30p136ZQ=
google.golang.org/api v0.88.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.89.0 h1:OUywo5UEEZ8H1eMy55mFpkL9Sy59mQ5TzYGWa+td8zo=
google.golang.org/api v0.89.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View file

@ -114,10 +114,6 @@ func (p *BoltProvider) validateUserAndTLSCert(username, protocol string, tlsCert
}
func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
var user User
if password == "" {
return user, errors.New("credentials cannot be null or empty")
}
user, err := p.userExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)

View file

@ -984,6 +984,12 @@ 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 {
if err := user.User.CheckLoginConditions(); err != nil {
return err
}
if loginMethod == LoginMethodPassword && user.User.Filters.IsAnonymous {
return nil
}
if loginMethod != LoginMethodPassword {
_, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert)
if err != nil {
@ -996,9 +1002,6 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco
return nil
}
}
if err := user.User.CheckLoginConditions(); err != nil {
return err
}
if password == "" {
return ErrInvalidCredentials
}
@ -2098,6 +2101,7 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
filters.DisableFsChecks = in.DisableFsChecks
filters.StartDirectory = in.StartDirectory
filters.FTPSecurity = in.FTPSecurity
filters.IsAnonymous = in.IsAnonymous
filters.AllowAPIKeyAuth = in.AllowAPIKeyAuth
filters.ExternalAuthCacheTime = in.ExternalAuthCacheTime
filters.WebClient = make([]string, len(in.WebClient))
@ -2608,6 +2612,9 @@ func validateBaseParams(user *User) error {
user.UploadDataTransfer = 0
user.DownloadDataTransfer = 0
}
if user.Filters.IsAnonymous {
user.setAnonymousSettings()
}
return user.FsConfig.Validate(user.GetEncryptionAdditionalData())
}
@ -2806,11 +2813,15 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if err != nil {
return *user, err
}
if user.Filters.IsAnonymous {
user.setAnonymousSettings()
return *user, nil
}
password, err = checkUserPasscode(user, password, protocol)
if err != nil {
return *user, ErrInvalidCredentials
}
if user.Password == "" {
if user.Password == "" || password == "" {
return *user, errors.New("credentials cannot be null or empty")
}
if !user.Filters.Hooks.CheckPasswordDisabled {

View file

@ -146,10 +146,6 @@ func (p *MemoryProvider) validateUserAndTLSCert(username, protocol string, tlsCe
}
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
var user User
if password == "" {
return user, errors.New("credentials cannot be null or empty")
}
user, err := p.userExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)

View file

@ -744,10 +744,6 @@ func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, err
}
func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHandle *sql.DB) (User, error) {
var user User
if password == "" {
return user, errors.New("credentials cannot be null or empty")
}
user, err := sqlCommonGetUserByUsername(username, dbHandle)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)

View file

@ -373,6 +373,20 @@ func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
return result
}
func (u *User) setAnonymousSettings() {
for k := range u.Permissions {
u.Permissions[k] = []string{PermListItems, PermDownload}
}
u.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, protocolSSH, protocolHTTP)
u.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols, false)
for _, method := range ValidLoginMethods {
if method != LoginMethodPassword {
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, method)
}
}
u.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods, false)
}
// RenderAsJSON implements the renderer interface used within plugins
func (u *User) RenderAsJSON(reload bool) ([]byte, error) {
if reload {
@ -1703,6 +1717,9 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *s
if !u.Filters.AllowAPIKeyAuth {
u.Filters.AllowAPIKeyAuth = filters.AllowAPIKeyAuth
}
if !u.Filters.IsAnonymous {
u.Filters.IsAnonymous = filters.IsAnonymous
}
if u.Filters.ExternalAuthCacheTime == 0 {
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
}

View file

@ -245,6 +245,7 @@ XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
tlsClient2Username = "client2"
httpFsPort = 23456
defaultHTTPFsUsername = "httpfs_ftp_user"
emptyPwdPlaceholder = "empty"
)
var (
@ -819,6 +820,144 @@ func TestStartDirectory(t *testing.T) {
assert.NoError(t, err)
}
func TestLoginEmptyPassword(t *testing.T) {
u := getTestUser()
u.Password = ""
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
user.Password = emptyPwdPlaceholder
_, err = getFTPClient(user, true, nil)
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestAnonymousUser(t *testing.T) {
u := getTestUser()
u.Password = ""
u.Filters.IsAnonymous = true
_, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.Error(t, err)
user, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
user.Password = emptyPwdPlaceholder
client, err := getFTPClient(user, true, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = os.Rename(testFilePath, filepath.Join(user.GetHomeDir(), testFileName))
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
err = client.MakeDir("adir")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = client.Quit()
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestAnonymousGroupInheritance(t *testing.T) {
g := getTestGroup()
g.UserSettings.Filters.IsAnonymous = true
g.UserSettings.Permissions = make(map[string][]string)
g.UserSettings.Permissions["/"] = allPerms
g.UserSettings.Permissions["/testsub"] = allPerms
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
assert.NoError(t, err)
u := getTestUser()
u.Groups = []sdk.GroupMapping{
{
Name: group.Name,
Type: sdk.GroupTypePrimary,
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
user.Password = emptyPwdPlaceholder
client, err := getFTPClient(user, true, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = client.MakeDir("adir")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = client.MakeDir("/testsub/adir")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = os.Rename(testFilePath, filepath.Join(user.GetHomeDir(), testFileName))
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
err = client.Quit()
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
user.Password = defaultPassword
client, err = getFTPClient(user, true, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
err := client.Quit()
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
assert.NoError(t, err)
}
func TestMultiFactorAuth(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -1139,6 +1278,98 @@ func TestPreLoginHook(t *testing.T) {
assert.NoError(t, err)
}
func TestPreLoginHookReturningAnonymousUser(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
u := getTestUser()
u.Filters.IsAnonymous = true
u.Filters.DeniedProtocols = []string{common.ProtocolSSH}
u.Password = ""
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
assert.NoError(t, err)
providerConf.PreLoginHook = preLoginPath
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
// the pre-login hook create the anonymous user
client, err := getFTPClient(u, false, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = client.MakeDir("tdiranonymous")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = os.Rename(testFilePath, filepath.Join(u.GetHomeDir(), testFileName))
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
err := client.Quit()
assert.NoError(t, err)
}
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
// now the same with an existing user
client, err = getFTPClient(u, false, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "permission")
}
err = os.Rename(testFilePath, filepath.Join(u.GetHomeDir(), testFileName))
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
err := client.Quit()
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
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(preLoginPath)
assert.NoError(t, err)
}
func TestPreDownloadHook(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
@ -3487,7 +3718,11 @@ func getFTPClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config) (*
}
pwd := defaultPassword
if user.Password != "" {
pwd = user.Password
if user.Password == emptyPwdPlaceholder {
pwd = ""
} else {
pwd = user.Password
}
}
err = client.Login(user.Username, pwd)
if err != nil {

View file

@ -232,6 +232,8 @@ func (s *Server) PreAuthUser(cc ftpserver.ClientContext, username string) error
return nil
}
if _, ok := err.(*util.RecordNotFoundError); !ok {
logger.Error(logSender, fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID()),
"unable to get user on pre auth: %v", err)
return common.ErrInternalFailure
}
}

View file

@ -1938,6 +1938,66 @@ func TestAdminTimestamps(t *testing.T) {
assert.NoError(t, err)
}
func TestHTTPUserAuthEmptyPassword(t *testing.T) {
u := getTestUser()
u.Password = ""
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
req.SetBasicAuth(defaultUsername, "")
c := httpclient.GetHTTPClient()
resp, err := c.Do(req)
c.CloseIdleConnections()
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, "")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unexpected status code 401")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestHTTPAnonymousUser(t *testing.T) {
u := getTestUser()
u.Filters.IsAnonymous = true
_, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.Error(t, err)
user, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
req.SetBasicAuth(defaultUsername, defaultPassword)
c := httpclient.GetHTTPClient()
resp, err := c.Do(req)
c.CloseIdleConnections()
assert.NoError(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unexpected status code 403")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestHTTPUserAuthentication(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -16901,6 +16961,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
form.Set("ftp_security", "1")
form.Set("external_auth_cache_time", "0")
form.Set("description", "desc %username% %password%")
form.Set("start_directory", "/base/%username%")
form.Set("vfolder_path", "/vdir%username%")
form.Set("vfolder_name", folder.Name)
form.Set("vfolder_quota_size", "-1")
@ -16956,6 +17017,8 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
assert.Equal(t, "desc auser2 password2", user2.Description)
assert.Equal(t, filepath.Join(os.TempDir(), user1.Username), user1.HomeDir)
assert.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir)
assert.Equal(t, path.Join("/base", user1.Username), user1.Filters.StartDirectory)
assert.Equal(t, path.Join("/base", user2.Username), user2.Filters.StartDirectory)
assert.Equal(t, folder.Name, folder1.Name)
assert.Equal(t, folder.MappedPath, folder1.MappedPath)
assert.Equal(t, folder.Description, folder1.Description)

View file

@ -1302,6 +1302,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
if util.Contains(hooks, "check_password_disabled") {
filters.Hooks.CheckPasswordDisabled = true
}
filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
filters.DisableFsChecks = r.Form.Get("disable_fs_checks") != ""
filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
filters.StartDirectory = r.Form.Get("start_directory")
@ -1618,6 +1619,7 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
user.VirtualFolders = vfolders
user.Description = replacePlaceholders(user.Description, replacements)
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
user.Filters.StartDirectory = replacePlaceholders(user.Filters.StartDirectory, replacements)
switch user.FsConfig.Provider {
case sdk.CryptedFilesystemProvider:

View file

@ -2069,6 +2069,9 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
if expected.FTPSecurity != actual.FTPSecurity {
return errors.New("ftp_security mismatch")
}
if expected.IsAnonymous != actual.IsAnonymous {
return errors.New("is_anonymous mismatch")
}
if err := compareUserFilterSubStructs(expected, actual); err != nil {
return err
}

View file

@ -2576,6 +2576,74 @@ func TestLoginWithIPFilters(t *testing.T) {
assert.NoError(t, err)
}
func TestLoginEmptyPassword(t *testing.T) {
usePubKey := false
u := getTestUser(usePubKey)
u.Password = ""
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
user.Password = "empty"
_, _, err = getSftpClient(user, usePubKey)
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestLoginAnonymousUser(t *testing.T) {
usePubKey := false
u := getTestUser(usePubKey)
u.Password = ""
u.Filters.IsAnonymous = true
_, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.Error(t, err)
user, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
_, _, err = getSftpClient(user, usePubKey)
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestAnonymousGroupInheritance(t *testing.T) {
g := getTestGroup()
g.UserSettings.Filters.IsAnonymous = true
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
assert.NoError(t, err)
usePubKey := false
u := getTestUser(usePubKey)
u.Groups = []sdk.GroupMapping{
{
Name: group.Name,
Type: sdk.GroupTypePrimary,
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, _, err = getSftpClient(user, usePubKey)
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
assert.NoError(t, err)
}
func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
usePubKey := false
user, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
@ -3980,6 +4048,62 @@ func TestLoginExternalAuthErrors(t *testing.T) {
assert.NoError(t, err)
}
func TestExternalAuthReturningAnonymousUser(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
usePubKey := false
u := getTestUser(usePubKey)
u.Filters.IsAnonymous = true
u.Password = ""
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm)
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
_, _, err = getSftpClient(u, usePubKey)
assert.Error(t, err)
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
// test again, the user now exists
_, _, err = getSftpClient(u, usePubKey)
assert.Error(t, err)
updatedUser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
user.UpdatedAt = updatedUser.UpdatedAt
assert.Equal(t, user, updatedUser)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
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 TestExternalAuthPreserveMFAConfig(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
@ -10572,7 +10696,11 @@ func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string)
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else {
if user.Password != "" {
config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}
if user.Password == "empty" {
config.Auth = []ssh.AuthMethod{ssh.Password("")}
} else {
config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}
}
} else {
config.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}
}

View file

@ -240,10 +240,11 @@ D17SEQKBgCKC0GjDjnt/JvujdzHuBt1sWdOtb+B6kQvA09qVmuDF/Dq36jiaHDjg
XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
2tmlLP54LaN35hQxRjhT8lCN0BkrNF44+bh8frwm/kuxSd8wT2S+
-----END RSA PRIVATE KEY-----`
testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat"
tlsClient1Username = "client1"
tlsClient2Username = "client2"
testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat"
tlsClient1Username = "client1"
tlsClient2Username = "client2"
emptyPwdPlaceholder = "empty"
)
var (
@ -691,6 +692,63 @@ func TestBasicHandlingCryptFs(t *testing.T) {
assert.Len(t, common.Connections.GetStats(), 0)
}
func TestLoginEmptyPassword(t *testing.T) {
u := getTestUser()
u.Password = ""
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
user.Password = emptyPwdPlaceholder
client := getWebDavClient(user, false, nil)
err = checkBasicFunc(client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "401")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestAnonymousUser(t *testing.T) {
u := getTestUser()
u.Password = ""
u.Filters.IsAnonymous = true
_, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.Error(t, err)
user, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)
assert.NoError(t, err)
client := getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
user.Password = emptyPwdPlaceholder
client = getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
false, testFileSize, client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
err = client.Mkdir("testdir", os.ModePerm)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestLockAfterDelete(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -950,7 +1008,7 @@ func TestLoginExternalAuth(t *testing.T) {
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm)
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
@ -979,6 +1037,151 @@ func TestLoginExternalAuth(t *testing.T) {
assert.NoError(t, err)
}
func TestExternalAuthReturningAnonymousUser(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
u := getTestUser()
u.Filters.IsAnonymous = true
u.Filters.DeniedProtocols = []string{common.ProtocolSSH}
u.Password = ""
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), 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))
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = uploadFileWithRawClient(testFilePath, testFileName, u.Username, emptyPwdPlaceholder,
false, testFileSize, client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.IsAnonymous)
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions["/"])
assert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)
assert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,
dataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)
u.Password = emptyPwdPlaceholder
client = getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
false, testFileSize, client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
err = client.Mkdir("testdir", os.ModePerm)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
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 TestExternalAuthAnonymousGroupInheritance(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
g := dataprovider.Group{
BaseGroup: sdk.BaseGroup{
Name: "test_group",
},
UserSettings: dataprovider.GroupUserSettings{
BaseGroupUserSettings: sdk.BaseGroupUserSettings{
Permissions: map[string][]string{
"/": allPerms,
},
Filters: sdk.BaseUserFilters{
IsAnonymous: true,
},
},
},
}
u := getTestUser()
u.Groups = []sdk.GroupMapping{
{
Name: g.Name,
Type: sdk.GroupTypePrimary,
},
}
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
assert.NoError(t, err)
u.Password = emptyPwdPlaceholder
client := getWebDavClient(u, false, nil)
assert.NoError(t, checkBasicFunc(client))
err = client.Mkdir("tdir", os.ModePerm)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
}
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.False(t, user.Filters.IsAnonymous)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group, 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 TestPreLoginHook(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
@ -2415,7 +2618,7 @@ func TestExternatAuthWithClientCert(t *testing.T) {
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm)
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
@ -2826,7 +3029,11 @@ func getWebDavClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config)
}
pwd := defaultPassword
if user.Password != "" {
pwd = user.Password
if user.Password == emptyPwdPlaceholder {
pwd = ""
} else {
pwd = user.Password
}
}
client := gowebdav.NewClient(rootPath, user.Username, pwd)
client.SetTimeout(10 * time.Second)
@ -2889,24 +3096,13 @@ func getEncryptedFileSize(size int64) (int64, error) {
return int64(encSize) + 33, err
}
func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse bool, username string) []byte {
func getExtAuthScriptContent(user dataprovider.User) []byte {
extAuthContent := []byte("#!/bin/sh\n\n")
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
if len(username) > 0 {
user.Username = username
}
u, _ := json.Marshal(user)
if nonJSONResponse {
extAuthContent = append(extAuthContent, []byte("echo 'text response'\n")...)
} else {
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("echo '%v'\n", string(u)))...)
}
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("echo '%v'\n", string(u)))...)
extAuthContent = append(extAuthContent, []byte("else\n")...)
if nonJSONResponse {
extAuthContent = append(extAuthContent, []byte("echo 'text response'\n")...)
} else {
extAuthContent = append(extAuthContent, []byte("echo '{\"username\":\"\"}'\n")...)
}
extAuthContent = append(extAuthContent, []byte("echo '{\"username\":\"\"}'\n")...)
extAuthContent = append(extAuthContent, []byte("fi\n")...)
return extAuthContent
}

View file

@ -4753,6 +4753,9 @@ components:
- 0
- 1
description: 'Set to `1` to require TLS for both data and control connection. his setting is useful if you want to allow both encrypted and plain text FTP sessions globally and then you want to require encrypted sessions on a per-user basis. It has no effect if TLS is already required for all users in the configuration file.'
is_anonymous:
type: boolean
description: 'If enabled the user can login with any password or no password at all. Anonymous users are supported for FTP and WebDAV protocols and permissions will be automatically set to "list" and "download" (read only)'
description: Additional user options
UserFilters:
allOf:

View file

@ -720,6 +720,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAnonymous" name="is_anonymous"
{{if .Group.UserSettings.Filters.IsAnonymous}}checked{{end}} aria-describedby="anonymousHelpBlock">
<label for="idAnonymous" class="form-check-label">Is Anonymous</label>
<small id="anonymousHelpBlock" class="form-text text-muted">
Anonymous users are supported for FTP and WebDAV protocols and have read-only access
</small>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"

View file

@ -952,6 +952,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAnonymous" name="is_anonymous"
{{if .User.Filters.IsAnonymous}}checked{{end}} aria-describedby="anonymousHelpBlock">
<label for="idAnonymous" class="form-check-label">Is Anonymous</label>
<small id="anonymousHelpBlock" class="form-text text-muted">
Anonymous users are supported for FTP and WebDAV protocols and have read-only access
</small>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"