From fdf3f23df59d92e9dd08c08231196e273fb7d449 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 4 Apr 2021 22:32:25 +0200 Subject: [PATCH] allow to disable some hooks on a per-user basis This way you can, for example, mix external and internal users --- dataprovider/dataprovider.go | 49 +++++++++++++++++++++--------------- dataprovider/user.go | 13 ++++++++++ docs/check-password-hook.md | 2 ++ docs/dynamic-user-mod.md | 2 ++ docs/external-auth.md | 2 ++ go.mod | 18 ++++++------- go.sum | 48 +++++++++++++++++------------------ httpd/httpd_test.go | 21 ++++++++++++++++ httpd/schema/openapi.yaml | 21 ++++++++++++++++ httpd/web.go | 10 ++++++++ httpdtest/httpdtest.go | 9 +++++++ sftpd/sftpd_test.go | 48 +++++++++++++++++++++++++++++++++++ templates/fsconfig.html | 2 +- templates/user.html | 17 +++++++++++++ vfs/sftpfs.go | 15 +++-------- 15 files changed, 211 insertions(+), 66 deletions(-) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 67f0b0ca..4915c80b 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -1601,23 +1601,25 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) { if user.Password == "" { return *user, errors.New("credentials cannot be null or empty") } - hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol) - if err != nil { - providerLog(logger.LevelDebug, "error executing check password hook: %v", err) - return *user, errors.New("unable to check credentials") - } - switch hookResponse.Status { - case -1: - // no hook configured - case 1: - providerLog(logger.LevelDebug, "password accepted by check password hook") - return *user, nil - case 2: - providerLog(logger.LevelDebug, "partial success from check password hook") - password = hookResponse.ToVerify - default: - providerLog(logger.LevelDebug, "password rejected by check password hook, status: %v", hookResponse.Status) - return *user, ErrInvalidCredentials + if !user.Filters.Hooks.CheckPasswordDisabled { + hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol) + if err != nil { + providerLog(logger.LevelDebug, "error executing check password hook: %v", err) + return *user, errors.New("unable to check credentials") + } + switch hookResponse.Status { + case -1: + // no hook configured + case 1: + providerLog(logger.LevelDebug, "password accepted by check password hook") + return *user, nil + case 2: + providerLog(logger.LevelDebug, "partial success from check password hook") + password = hookResponse.ToVerify + default: + providerLog(logger.LevelDebug, "password rejected by check password hook, status: %v", hookResponse.Status) + return *user, ErrInvalidCredentials + } } match, err := isPasswordOK(user, password) @@ -2142,6 +2144,9 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro if err != nil { return u, err } + if u.Filters.Hooks.PreLoginDisabled { + return u, nil + } startTime := time.Now() out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON) if err != nil { @@ -2328,11 +2333,16 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) { func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, tlsCert *x509.Certificate) (User, error) { var user User - pkey, err := utils.GetSSHPublicKeyAsString(pubKey) + u, userAsJSON, err := getUserAndJSONForHook(username) if err != nil { return user, err } - u, userAsJSON, err := getUserAndJSONForHook(username) + + if u.Filters.Hooks.ExternalAuthDisabled { + return u, nil + } + + pkey, err := utils.GetSSHPublicKeyAsString(pubKey) if err != nil { return user, err } @@ -2397,7 +2407,6 @@ func getUserAndJSONForHook(username string) (User, []byte, error) { ID: 0, Username: username, } - return u, userAsJSON, nil } userAsJSON, err = json.Marshal(u) if err != nil { diff --git a/dataprovider/user.go b/dataprovider/user.go index 69a850a8..b114dbb8 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -116,7 +116,15 @@ type PatternsFilter struct { DeniedPatterns []string `json:"denied_patterns,omitempty"` } +// HooksFilter defines user specific overrides for global hooks +type HooksFilter struct { + ExternalAuthDisabled bool `json:"external_auth_disabled"` + PreLoginDisabled bool `json:"pre_login_disabled"` + CheckPasswordDisabled bool `json:"check_password_disabled"` +} + // UserFilters defines additional restrictions for a user +// TODO: rename to UserOptions in v3 type UserFilters struct { // only clients connecting from these IP/Mask are allowed. // IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291 @@ -142,6 +150,8 @@ type UserFilters struct { // For FTP clients it must match the name provided using the // "USER" command TLSUsername TLSUsername `json:"tls_username,omitempty"` + // user specific hook overrides + Hooks HooksFilter `json:"hooks,omitempty"` } // User defines a SFTPGo user @@ -1069,6 +1079,9 @@ func (u *User) getACopy() User { copy(filters.FilePatterns, u.Filters.FilePatterns) filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols)) copy(filters.DeniedProtocols, u.Filters.DeniedProtocols) + filters.Hooks.ExternalAuthDisabled = u.Filters.Hooks.ExternalAuthDisabled + filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled + filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled return User{ ID: u.ID, diff --git a/docs/check-password-hook.md b/docs/check-password-hook.md index 7952c6b3..5874bb53 100644 --- a/docs/check-password-hook.md +++ b/docs/check-password-hook.md @@ -42,4 +42,6 @@ You can also restrict the hook scope using the `check_password_scope` configurat You can combine the scopes. For example, 6 means FTP and WebDAV. +You can disable the hook on a per-user basis. + An example check password program allowing 2FA using password + one time token can be found inside the source tree [checkpwd](../examples/OTP/authy/checkpwd) directory. diff --git a/docs/dynamic-user-mod.md b/docs/dynamic-user-mod.md index 9c4226e4..989a2d14 100644 --- a/docs/dynamic-user-mod.md +++ b/docs/dynamic-user-mod.md @@ -35,6 +35,8 @@ If an error happens while executing the hook then login will be denied. "Dynamic user creation or modification" and "External Authentication" are mutually exclusive, they are quite similar, the difference is that "External Authentication" returns an already authenticated user while using "Dynamic users modification" you simply create or update a user. The authentication will be checked inside SFTPGo. In other words while using "External Authentication" the external program receives the credentials of the user trying to login (for example the cleartext password) and it needs to validate them. While using "Dynamic users modification" the pre-login program receives the user stored inside the dataprovider (it includes the hashed password if any) and it can modify it, after the modification SFTPGo will check the credentials of the user trying to login. +You can disable the hook on a per-user basis. + Let's see a very basic example. Our sample program will grant access to the existing user `test_user` only in the time range 10:00-18:00. Other users will not be modified since the program will terminate with no output. ```shell diff --git a/docs/external-auth.md b/docs/external-auth.md index f8774dc8..db8d0355 100644 --- a/docs/external-auth.md +++ b/docs/external-auth.md @@ -68,6 +68,8 @@ fi The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml). +You can disable the hook on a per-user basis so that you can mix external and internal users. + An example authentication program allowing to authenticate against an LDAP server can be found inside the source tree [ldapauth](../examples/ldapauth) directory. An example server, to use as HTTP authentication hook, allowing to authenticate against an LDAP server can be found inside the source tree [ldapauthserver](../examples/ldapauthserver) directory. diff --git a/go.mod b/go.mod index b883c163..0fe80f4c 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,23 @@ module github.com/drakkan/sftpgo go 1.16 require ( - cloud.google.com/go v0.80.0 // indirect + cloud.google.com/go v0.81.0 // indirect cloud.google.com/go/storage v1.14.0 github.com/Azure/azure-storage-blob-go v0.13.0 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77 - github.com/aws/aws-sdk-go v1.38.7 + github.com/aws/aws-sdk-go v1.38.12 github.com/cockroachdb/cockroach-go/v2 v2.1.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d github.com/fclairamb/ftpserverlib v0.13.0 github.com/frankban/quicktest v1.11.3 // indirect github.com/go-chi/chi/v5 v5.0.2 - github.com/go-chi/jwtauth/v5 v5.0.0 + github.com/go-chi/jwtauth/v5 v5.0.1 github.com/go-chi/render v1.0.1 github.com/go-ole/go-ole v1.2.5 // indirect - github.com/go-sql-driver/mysql v1.5.0 + github.com/go-sql-driver/mysql v1.6.0 github.com/golang/snappy v0.0.3 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.2.0 // indirect @@ -31,7 +31,7 @@ require ( github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect - github.com/lestrrat-go/jwx v1.1.6 + github.com/lestrrat-go/jwx v1.1.7 github.com/lib/pq v1.10.0 github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-sqlite3 v1.14.6 @@ -46,10 +46,10 @@ require ( github.com/prometheus/client_golang v1.10.0 github.com/prometheus/common v0.20.0 // indirect github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b - github.com/rs/xid v1.2.1 + github.com/rs/xid v1.3.0 github.com/rs/zerolog v1.21.0 github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil/v3 v3.21.2 + github.com/shirou/gopsutil/v3 v3.21.3 github.com/spf13/afero v1.6.0 github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.1.3 @@ -65,8 +65,8 @@ require ( golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 - golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect + golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/api v0.43.0 gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/go.sum b/go.sum index 2db55044..7206d09b 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.80.0 h1:kAdyAMrj9CjqOSGiluseVjIgAyQ3uxADYtUYR6MwYeY= -cloud.google.com/go v0.80.0/go.mod h1:fqpb6QRi1CFGAMXDoE72G+b+Ybv7dMB/T1tbExDHktI= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -118,8 +118,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.38.7 h1:uOu2IrTiNhcSNAjBmA21t48lTx5mgGdcFKamDjXMscA= -github.com/aws/aws-sdk-go v1.38.7/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.38.12 h1:khtODkUna3iF53Cg3dCF4e6oWgrAEbZDU4x1aq+G0WY= +github.com/aws/aws-sdk-go v1.38.12/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -219,8 +219,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.2 h1:4xKeALZdMEsuI5s05PU2Bm89Uc5iM04qFubUCl5LfAQ= github.com/go-chi/chi/v5 v5.0.2/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/jwtauth/v5 v5.0.0 h1:FjyoBQ0sH6/OSBCTXdRMMd7Eis3UjmsX2wKTSucFn+g= -github.com/go-chi/jwtauth/v5 v5.0.0/go.mod h1:wdYCsXCBuihmcGwLdfVgZ4LhDLOZHfyF+Fd5mEjiGPM= +github.com/go-chi/jwtauth/v5 v5.0.1 h1:eyJ6Yx5VphEfjkqpZ7+LJEWThzyIcF5aN2QVpgqSIu0= +github.com/go-chi/jwtauth/v5 v5.0.1/go.mod h1:+JtcRYGZsnA4+ur1LFlb4Bei3O9WeUzoMfDZWfUJuoY= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -246,14 +246,14 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.3.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.4.8 h1:TfwOxfSp8hXH+ivoOk36RyDNmXATUETRdaNWDaZglf8= github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -517,15 +517,16 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= -github.com/lestrrat-go/iter v1.0.0/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.1.0/go.mod h1:vn9FzD6gJtKkgYs7RTKV7CjWtEka8F/voUollhnn4QE= -github.com/lestrrat-go/jwx v1.1.6 h1:VfyUo2PAU4lO/liwhdwiSZ55/QZDLTT3EYY5z9KfwZs= github.com/lestrrat-go/jwx v1.1.6/go.mod h1:c+R8G7qsaFNmTzYjU98A+sMh8Bo/MJqO9GnpqR+X024= +github.com/lestrrat-go/jwx v1.1.7 h1:+PNt2U7FfrK4xn+ZCG+9jPRq5eqyG30gwpVwcekrCjA= +github.com/lestrrat-go/jwx v1.1.7/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc= github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -688,8 +689,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b h1:LeFDRLtjhSBjIezNZvfN0CHsu2GfDS2CJAiEGaWBJ34= github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= -github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM= @@ -706,8 +708,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= -github.com/shirou/gopsutil/v3 v3.21.2 h1:fIOk3hyqV1oGKogfGNjUZa0lUbtlkx3+ZT0IoJth2uM= -github.com/shirou/gopsutil/v3 v3.21.2/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= +github.com/shirou/gopsutil/v3 v3.21.3 h1:wgcdAHZS2H6qy4JFewVTtqfiYxFzCeEJod/mLztdPG8= +github.com/shirou/gopsutil/v3 v3.21.3/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -857,8 +859,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -932,11 +934,10 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 h1:64ChN/hjER/taL4YJuA+gpLfIMT+/NFherRZixbxOhg= -golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1053,7 +1054,6 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.42.0/go.mod h1:+Oj4s6ch2SEGtPjGqfUfZonBH0GjQH89gTeKKAEGZKI= google.golang.org/api v0.43.0 h1:4sAyIHT6ZohtAQDoxws+ez7bROYmUlOVvsUscYCDTqA= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1112,10 +1112,9 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a h1:XVaQ1+BDKvrRcgppHhtAaniHCKyV5xJAvymwsPHHFaE= -google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 h1:E7wSQBXkH3T3diucK+9Z1kjn4+/9tNG7lZLr75oOhh8= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1140,8 +1139,9 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 184f5283..298f9a55 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -974,6 +974,7 @@ func TestUpdateUser(t *testing.T) { u.UsedQuotaFiles = 1 u.UsedQuotaSize = 2 u.Filters.TLSUsername = dataprovider.TLSUsernameCN + u.Filters.Hooks.CheckPasswordDisabled = true user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) assert.Equal(t, 0, user.UsedQuotaFiles) @@ -991,6 +992,9 @@ func TestUpdateUser(t *testing.T) { user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} user.Filters.DeniedProtocols = []string{common.ProtocolWebDAV} user.Filters.TLSUsername = dataprovider.TLSUsernameNone + user.Filters.Hooks.ExternalAuthDisabled = true + user.Filters.Hooks.PreLoginDisabled = true + user.Filters.Hooks.CheckPasswordDisabled = false user.Filters.FileExtensions = append(user.Filters.FileExtensions, dataprovider.ExtensionsFilter{ Path: "/subdir", AllowedExtensions: []string{".zip", ".rar"}, @@ -1027,6 +1031,7 @@ func TestUpdateUser(t *testing.T) { }) user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) + _, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "invalid") assert.NoError(t, err) user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "0") @@ -4852,6 +4857,7 @@ func TestWebUserAddMock(t *testing.T) { form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv") form.Set("additional_info", user.AdditionalInfo) form.Set("description", user.Description) + form.Add("hooks", "external_auth_disabled") b, contentType, _ := getMultipartFormData(form, "", "") // test invalid url escape req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b) @@ -5011,6 +5017,9 @@ func TestWebUserAddMock(t *testing.T) { assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize) assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo) assert.Equal(t, user.Description, newUser.Description) + assert.True(t, newUser.Filters.Hooks.ExternalAuthDisabled) + assert.False(t, newUser.Filters.Hooks.PreLoginDisabled) + assert.False(t, newUser.Filters.Hooks.CheckPasswordDisabled) assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys)) if val, ok := newUser.Permissions["/subdir"]; ok { assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val)) @@ -5416,6 +5425,8 @@ func TestUserTemplateMock(t *testing.T) { form.Set("allowed_extensions", "/dir1::.jpg,.png") form.Set("denied_extensions", "/dir2::.zip") form.Set("max_upload_file_size", "0") + form.Add("hooks", "external_auth_disabled") + form.Add("hooks", "check_password_disabled") // test invalid s3_upload_part_size form.Set("s3_upload_part_size", "a") b, contentType, _ := getMultipartFormData(form, "", "") @@ -5487,6 +5498,12 @@ func TestUserTemplateMock(t *testing.T) { err = user2.FsConfig.S3Config.AccessSecret.Decrypt() require.NoError(t, err) require.Equal(t, "password2", user2.FsConfig.S3Config.AccessSecret.GetPayload()) + require.True(t, user1.Filters.Hooks.ExternalAuthDisabled) + require.True(t, user1.Filters.Hooks.CheckPasswordDisabled) + require.False(t, user1.Filters.Hooks.PreLoginDisabled) + require.True(t, user2.Filters.Hooks.ExternalAuthDisabled) + require.True(t, user2.Filters.Hooks.CheckPasswordDisabled) + require.False(t, user2.Filters.Hooks.PreLoginDisabled) } func TestFolderTemplateMock(t *testing.T) { @@ -5663,6 +5680,7 @@ func TestWebUserS3Mock(t *testing.T) { form.Set("denied_extensions", "/dir2::.zip") form.Set("max_upload_file_size", "0") form.Set("description", user.Description) + form.Add("hooks", "pre_login_disabled") // test invalid s3_upload_part_size form.Set("s3_upload_part_size", "a") b, contentType, _ := getMultipartFormData(form, "", "") @@ -5710,6 +5728,9 @@ func TestWebUserS3Mock(t *testing.T) { assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey()) assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData()) assert.Equal(t, user.Description, updateUser.Description) + assert.True(t, updateUser.Filters.Hooks.PreLoginDisabled) + assert.False(t, updateUser.Filters.Hooks.ExternalAuthDisabled) + assert.False(t, updateUser.Filters.Hooks.CheckPasswordDisabled) // now check that a redacted password is not saved form.Set("s3_access_secret", redactedSecret) b, contentType, _ = getMultipartFormData(form, "", "") diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 7ece3dbe..42aa4b7d 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -1421,6 +1421,22 @@ components: description: 'list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones' example: - .zip + HooksFilter: + type: object + properties: + external_auth_disabled: + type: boolean + example: false + description: If true, the external auth hook, if defined, will not be executed + pre_login_disabled: + type: boolean + example: false + description: If true, the pre-login hook, if defined, will not be executed + check_password_disabled: + type: boolean + example: false + description: If true, the check password hook, if defined, will not be executed + description: User specific hook overrides UserFilters: type: object properties: @@ -1469,6 +1485,8 @@ components: - None - CommonName description: 'defines the TLS certificate field to use as username. For FTP clients it must match the name provided using the "USER" command. For WebDAV, if no username is provided, the CN will be used as username. For WebDAV clients it must match the implicit or provided username. Ignored if mutual TLS is disabled' + hooks: + $ref: '#/components/schemas/HooksFilter' description: Additional user restrictions Secret: type: object @@ -1615,6 +1633,9 @@ components: description: Concurrent reads are safe to use and disabling them will degrade performance. Some servers automatically delete files once they are downloaded. Using concurrent reads is problematic with such servers. buffer_size: type: integer + minimum: 0 + maximum: 16 + example: 2 description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled. FilesystemConfig: type: object diff --git a/httpd/web.go b/httpd/web.go index 9cd83d56..3931ca31 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -662,6 +662,16 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters { filters.FileExtensions = getFileExtensionsFromPostField(r.Form.Get("allowed_extensions"), r.Form.Get("denied_extensions")) filters.FilePatterns = getFilePatternsFromPostField(r.Form.Get("allowed_patterns"), r.Form.Get("denied_patterns")) filters.TLSUsername = dataprovider.TLSUsername(r.Form.Get("tls_username")) + hooks := r.Form["hooks"] + if utils.IsStringInSlice("external_auth_disabled", hooks) { + filters.Hooks.ExternalAuthDisabled = true + } + if utils.IsStringInSlice("pre_login_disabled", hooks) { + filters.Hooks.PreLoginDisabled = true + } + if utils.IsStringInSlice("check_password_disabled", hooks) { + filters.Hooks.CheckPasswordDisabled = true + } return filters } diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index 5045fb67..3469f3d5 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -1168,6 +1168,15 @@ func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovid return errors.New("denied protocols contents mismatch") } } + if expected.Filters.Hooks.ExternalAuthDisabled != actual.Filters.Hooks.ExternalAuthDisabled { + return errors.New("external_auth_disabled hook mismatch") + } + if expected.Filters.Hooks.PreLoginDisabled != actual.Filters.Hooks.PreLoginDisabled { + return errors.New("pre_login_disabled hook mismatch") + } + if expected.Filters.Hooks.CheckPasswordDisabled != actual.Filters.Hooks.CheckPasswordDisabled { + return errors.New("check_password_disabled hook mismatch") + } return nil } diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 9def2fe6..2a238766 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -2084,6 +2084,20 @@ func TestPreLoginScript(t *testing.T) { if !assert.Error(t, err, "pre-login script returned a non json response, login must fail") { client.Close() } + // now disable the the hook + user.Filters.Hooks.PreLoginDisabled = true + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + client, err = getSftpClient(u, usePubKey) + if assert.NoError(t, err) { + defer client.Close() + assert.NoError(t, checkBasicSFTP(client)) + } + + user.Filters.Hooks.PreLoginDisabled = false + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + user.Status = 0 err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm) assert.NoError(t, err) @@ -2091,6 +2105,7 @@ func TestPreLoginScript(t *testing.T) { if !assert.Error(t, err, "pre-login script returned a disabled user, login must fail") { client.Close() } + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) @@ -2230,6 +2245,22 @@ func TestCheckPwdHook(t *testing.T) { client.Close() } + // now disable the the hook + user.Filters.Hooks.CheckPasswordDisabled = true + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + client, err = getSftpClient(user, usePubKey) + if assert.NoError(t, err) { + err = checkBasicSFTP(client) + assert.NoError(t, err) + client.Close() + } + + // enable the hook again + user.Filters.Hooks.CheckPasswordDisabled = false + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + err = os.WriteFile(checkPwdPath, getCheckPwdScriptsContents(1, ""), os.ModePerm) assert.NoError(t, err) user.Password = defaultPassword + "1" @@ -2323,6 +2354,23 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { assert.Equal(t, testFileSize, user.UsedQuotaSize) assert.Equal(t, 1, user.UsedQuotaFiles) + u.Status = 0 + err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm) + assert.NoError(t, err) + client, err = getSftpClient(u, usePubKey) + if !assert.Error(t, err) { + client.Close() + } + // now disable the the hook + user.Filters.Hooks.ExternalAuthDisabled = true + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + client, err = getSftpClient(u, usePubKey) + if assert.NoError(t, err) { + defer client.Close() + assert.NoError(t, checkBasicSFTP(client)) + } + _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) diff --git a/templates/fsconfig.html b/templates/fsconfig.html index 0349b699..18041348 100644 --- a/templates/fsconfig.html +++ b/templates/fsconfig.html @@ -251,7 +251,7 @@
+ value="{{.SFTPConfig.BufferSize}}" min="0" max="16" aria-describedby="SFTPBufferHelpBlock"> A buffer size > 0 enables concurrent transfers diff --git a/templates/user.html b/templates/user.html index 8058c131..30c30d2a 100644 --- a/templates/user.html +++ b/templates/user.html @@ -360,6 +360,23 @@
+
+ +
+ +
+
+ {{template "fshtml" .User.FsConfig}}
diff --git a/vfs/sftpfs.go b/vfs/sftpfs.go index 257896f2..29fffb06 100644 --- a/vfs/sftpfs.go +++ b/vfs/sftpfs.go @@ -109,8 +109,8 @@ func (c *SFTPFsConfig) Validate() error { if c.Username == "" { return errors.New("username cannot be empty") } - if c.BufferSize < 0 || c.BufferSize > 64 { - return errors.New("invalid buffer_size, valid range is 0-64") + if c.BufferSize < 0 || c.BufferSize > 16 { + return errors.New("invalid buffer_size, valid range is 0-16") } if err := c.validateCredentials(); err != nil { return err @@ -262,16 +262,7 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f return nil, nil, nil, err } go func() { - var n int64 - var err error - if fs.config.DisableCouncurrentReads { - n, err = fs.copy(w, f) - } else { - br := bufio.NewReaderSize(f, int(fs.config.BufferSize)*1024*1024) - // we don't use io.Copy since bufio.Reader implements io.ReadFrom and - // so it calls the sftp.File ReadFrom method without buffering - n, err = fs.copy(w, br) - } + n, err := io.Copy(w, f) w.CloseWithError(err) //nolint:errcheck f.Close() fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err)