diff --git a/docs/groups.md b/docs/groups.md index 9ae0865e..813a0fe4 100644 --- a/docs/groups.md +++ b/docs/groups.md @@ -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: diff --git a/go.mod b/go.mod index 65b790fa..2e222d57 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 1acf73bd..aa035ddc 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/dataprovider/bolt.go b/internal/dataprovider/bolt.go index eb56e0aa..a6926e9f 100644 --- a/internal/dataprovider/bolt.go +++ b/internal/dataprovider/bolt.go @@ -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) diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index e7c4523a..b6a7a9fc 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -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 { diff --git a/internal/dataprovider/memory.go b/internal/dataprovider/memory.go index 385aee49..e0f09b67 100644 --- a/internal/dataprovider/memory.go +++ b/internal/dataprovider/memory.go @@ -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) diff --git a/internal/dataprovider/sqlcommon.go b/internal/dataprovider/sqlcommon.go index 617ebbe6..a2a6e3da 100644 --- a/internal/dataprovider/sqlcommon.go +++ b/internal/dataprovider/sqlcommon.go @@ -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) diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go index d6aa6307..aad24f7c 100644 --- a/internal/dataprovider/user.go +++ b/internal/dataprovider/user.go @@ -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 } diff --git a/internal/ftpd/ftpd_test.go b/internal/ftpd/ftpd_test.go index e2c13019..63001373 100644 --- a/internal/ftpd/ftpd_test.go +++ b/internal/ftpd/ftpd_test.go @@ -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 { diff --git a/internal/ftpd/server.go b/internal/ftpd/server.go index 5adfc82b..85968cee 100644 --- a/internal/ftpd/server.go +++ b/internal/ftpd/server.go @@ -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 } } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index c4da700a..4660196a 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -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) diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 46355f65..38a65fda 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -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: diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index c3a12aae..40ee67a4 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -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 } diff --git a/internal/sftpd/sftpd_test.go b/internal/sftpd/sftpd_test.go index edbb92eb..f37880f8 100644 --- a/internal/sftpd/sftpd_test.go +++ b/internal/sftpd/sftpd_test.go @@ -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)} } diff --git a/internal/webdavd/webdavd_test.go b/internal/webdavd/webdavd_test.go index 7000e4b7..f36c0d1d 100644 --- a/internal/webdavd/webdavd_test.go +++ b/internal/webdavd/webdavd_test.go @@ -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 } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6dff4bfa..122f6528 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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: diff --git a/templates/webadmin/group.html b/templates/webadmin/group.html index 3b740f31..88c90805 100644 --- a/templates/webadmin/group.html +++ b/templates/webadmin/group.html @@ -720,6 +720,17 @@ along with this program. If not, see . +
+
+ + + + Anonymous users are supported for FTP and WebDAV protocols and have read-only access + +
+
+
.
+
+
+ + + + Anonymous users are supported for FTP and WebDAV protocols and have read-only access + +
+
+