mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
add support for anonymous users
Fixes #935 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e3c5cf981f
commit
ef0a3bc571
18 changed files with 723 additions and 52 deletions
|
@ -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
10
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
|
||||
)
|
||||
|
|
15
go.sum
15
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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue