ftpd: allow to require TLS on a per-user basis
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
81de7d271e
commit
ec5da8b4a5
15 changed files with 182 additions and 21 deletions
|
@ -6,7 +6,7 @@ To enable dynamic user modification, you must set the absolute path of your prog
|
|||
The external program can read the following environment variables to get info about the user trying to login:
|
||||
|
||||
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exist inside SFTPGo
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`, `IDP` (external identity provider)
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`, `IDP` (external identity provider) or empty if the hook is executed after receiving the FTP `USER` command
|
||||
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
|
||||
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
|
||||
|
@ -35,7 +35,9 @@ 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.
|
||||
|
||||
For SFTPGo users (not admins) authenticating using an external identity provider such as OpenID Connect, the pre-login hook will be executed after a successful authentication against the external IDP so that you can create/update the SFTPGo user matching the one authenticated against the identity provider. This is the only case where the pre-login hook is executed even if an external authentication hook is defined.
|
||||
For SFTPGo users (not admins) authenticating using an external identity provider such as OpenID Connect, the pre-login hook will be executed after a successful authentication against the external IDP so that you can create/update the SFTPGo user matching the one authenticated against the identity provider. In this case where the pre-login hook is executed even if an external authentication hook is defined.
|
||||
|
||||
If you enable FTP and allow both encrypted and plain text sessions, the pre-login hook is executed after receiving the FTP `USER` command. If you return an SFTPGo user with `ftp_security` set to `1` and the FTP session is not encrypted, it will be terminated. In this case where the pre-login hook is executed even if an external authentication hook is defined.
|
||||
|
||||
You can disable the hook on a per-user basis.
|
||||
|
||||
|
|
|
@ -13,7 +13,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: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`
|
||||
- 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
|
||||
- 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
|
||||
|
||||
|
|
14
go.mod
14
go.mod
|
@ -20,7 +20,7 @@ require (
|
|||
github.com/cockroachdb/cockroach-go/v2 v2.2.15
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220726122738-5dc4d4ff4940
|
||||
github.com/fclairamb/go-log v0.3.0
|
||||
github.com/go-acme/lego/v4 v4.8.0
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6
|
||||
|
@ -31,7 +31,7 @@ require (
|
|||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/hashicorp/go-hclog v1.2.1
|
||||
github.com/hashicorp/go-hclog v1.2.2
|
||||
github.com/hashicorp/go-plugin v1.4.4
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
|
@ -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.20220611083241-b653555f7f4d
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220726072922-52d01129ff42
|
||||
github.com/shirou/gopsutil/v3 v3.22.6
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.5.0
|
||||
|
@ -66,7 +66,7 @@ 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-20220722155237-a158d28d115b
|
||||
golang.org/x/net v0.0.0-20220725212005-46097bf591d3
|
||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
|
||||
|
@ -111,7 +111,7 @@ require (
|
|||
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
|
||||
|
@ -138,7 +138,7 @@ require (
|
|||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
@ -154,7 +154,7 @@ require (
|
|||
golang.org/x/tools v0.1.11 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b // indirect
|
||||
google.golang.org/grpc v1.48.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
|
|
23
go.sum
23
go.sum
|
@ -281,8 +281,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e h1:D7/to1KmKRTTRQyExulywEVYKhB+/WOW3gqiKimrbXg=
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e/go.mod h1:Ff6D1Ofy7/ezi7C30NPEgazzp/AQqyp0T8D7k+Tv2ls=
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220726122738-5dc4d4ff4940 h1:6/OqUlKsNadrpUMn+jZHQCp4Z5CubQqwc48pXiOlMmg=
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220726122738-5dc4d4ff4940/go.mod h1:YsDCUizF6tfRwX1rQ2k4cGZk22XynMmRXh+QxOCroPc=
|
||||
github.com/fclairamb/go-log v0.3.0 h1:oSC7Zjt0FZIYC5xXahUUycKGkypSdr2srFPLsp7CLd0=
|
||||
github.com/fclairamb/go-log v0.3.0/go.mod h1:XG61EiPlAXnPDN8SA4N3zeA+GyBJmVOCCo12WORx/gA=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
|
@ -459,8 +459,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
|
|||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
|
||||
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M=
|
||||
github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
|
||||
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||
|
@ -469,8 +469,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/yamux v0.1.0 h1:DzDIF6Sd7GD2sX0kDFpHAsJMY4L+OfTvtuaQsOYXxzk=
|
||||
github.com/hashicorp/yamux v0.1.0/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
|
@ -686,8 +686,9 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
|||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
@ -707,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.20220611083241-b653555f7f4d h1:gpshxOhLsGFbCy4ke96X8FVMy4xvXZQChSF7dikqxp4=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
|
||||
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/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=
|
||||
|
@ -1222,8 +1223,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
|
|||
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252 h1:G5AjFxR+ibe9Taamo0TdW+iylfBYK10DSkHYdx7PZ9w=
|
||||
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
|
||||
google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b h1:SfSkJugek6xm7lWywqth4r2iTrYLpD8lOj1nMIIhMNM=
|
||||
google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
|
|
@ -1193,6 +1193,25 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
|
|||
return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol)
|
||||
}
|
||||
|
||||
// GetFTPPreAuthUser returns the SFTPGo user with the specified username
|
||||
// after receiving the FTP "USER" command.
|
||||
// If a pre-login hook is defined it will be executed so the SFTPGo user
|
||||
// can be created if it does not exist
|
||||
func GetFTPPreAuthUser(username, ip string) (User, error) {
|
||||
var user User
|
||||
var err error
|
||||
if config.PreLoginHook != "" {
|
||||
user, err = executePreLoginHook(username, "", ip, protocolFTP, nil)
|
||||
} else {
|
||||
user, err = UserExists(username)
|
||||
}
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
err = user.LoadAndApplyGroupSettings()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// GetUserAfterIDPAuth returns the SFTPGo user with the specified username
|
||||
// after a successful authentication with an external identity provider.
|
||||
// If a pre-login hook is defined it will be executed so the SFTPGo user
|
||||
|
@ -2078,6 +2097,7 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
|
|||
filters.Hooks.CheckPasswordDisabled = in.Hooks.CheckPasswordDisabled
|
||||
filters.DisableFsChecks = in.DisableFsChecks
|
||||
filters.StartDirectory = in.StartDirectory
|
||||
filters.FTPSecurity = in.FTPSecurity
|
||||
filters.AllowAPIKeyAuth = in.AllowAPIKeyAuth
|
||||
filters.ExternalAuthCacheTime = in.ExternalAuthCacheTime
|
||||
filters.WebClient = make([]string, len(in.WebClient))
|
||||
|
|
|
@ -1706,6 +1706,9 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *s
|
|||
if u.Filters.ExternalAuthCacheTime == 0 {
|
||||
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
|
||||
}
|
||||
if u.Filters.FTPSecurity == 0 {
|
||||
u.Filters.FTPSecurity = filters.FTPSecurity
|
||||
}
|
||||
if u.Filters.StartDirectory == "" {
|
||||
u.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory, replacer)
|
||||
}
|
||||
|
|
|
@ -926,6 +926,61 @@ func TestLoginNonExistentUser(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFTPSecurity(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.FTPSecurity = 1
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
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 = getFTPClient(user, false, nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "TLS is required")
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGroupFTPSecurity(t *testing.T) {
|
||||
g := getTestGroup()
|
||||
g.UserSettings.Filters.FTPSecurity = 1
|
||||
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)
|
||||
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 = getFTPClient(user, false, nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "TLS is required")
|
||||
}
|
||||
_, 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 TestLoginExternalAuth(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
|
@ -1055,6 +1110,19 @@ func TestPreLoginHook(t *testing.T) {
|
|||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
user.Status = 0
|
||||
user.Filters.FTPSecurity = 1
|
||||
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(u, true, nil)
|
||||
if !assert.Error(t, err) {
|
||||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = getFTPClient(user, false, nil)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "TLS is required")
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -325,6 +325,10 @@ func (cc mockFTPClientContext) HasTLSForTransfers() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (cc mockFTPClientContext) SetTLSRequirement(requirement ftpserver.TLSRequirement) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc mockFTPClientContext) GetLastCommand() string {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -221,6 +221,23 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
|
|||
return connection, nil
|
||||
}
|
||||
|
||||
// PreAuthUser implements the MainDriverExtensionUserVerifier interface
|
||||
func (s *Server) PreAuthUser(cc ftpserver.ClientContext, username string) error {
|
||||
if s.binding.TLSMode == 0 && s.tlsConfig != nil {
|
||||
user, err := dataprovider.GetFTPPreAuthUser(username, util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))
|
||||
if err == nil {
|
||||
if user.Filters.FTPSecurity == 1 {
|
||||
return cc.SetTLSRequirement(ftpserver.MandatoryEncryption)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(*util.RecordNotFoundError); !ok {
|
||||
return common.ErrInternalFailure
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WrapPassiveListener implements the MainDriverExtensionPassiveWrapper interface
|
||||
func (s *Server) WrapPassiveListener(listener net.Listener) (net.Listener, error) {
|
||||
if s.binding.HasProxy() {
|
||||
|
|
|
@ -16513,6 +16513,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
assert.False(t, newUser.Filters.AllowAPIKeyAuth)
|
||||
assert.Equal(t, user.Email, newUser.Email)
|
||||
assert.Equal(t, "/start/dir", newUser.Filters.StartDirectory)
|
||||
assert.Equal(t, 0, newUser.Filters.FTPSecurity)
|
||||
assert.True(t, util.Contains(newUser.PublicKeys, testPubKey))
|
||||
if val, ok := newUser.Permissions["/subdir"]; ok {
|
||||
assert.True(t, util.Contains(val, dataprovider.PermListItems))
|
||||
|
@ -16897,6 +16898,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("fs_provider", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("description", "desc %username% %password%")
|
||||
form.Set("vfolder_path", "/vdir%username%")
|
||||
|
@ -16963,6 +16965,8 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
assert.Len(t, user2.VirtualFolders, 1)
|
||||
assert.Equal(t, "/vdirauser1", user1.VirtualFolders[0].VirtualPath)
|
||||
assert.Equal(t, "/vdirauser2", user2.VirtualFolders[0].VirtualPath)
|
||||
assert.Equal(t, 1, user1.Filters.FTPSecurity)
|
||||
assert.Equal(t, 1, user2.Filters.FTPSecurity)
|
||||
|
||||
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -17550,6 +17554,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("pattern_type1", "denied")
|
||||
form.Set("pattern_policy1", "1")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
form.Set("s3_force_path_style", "checked")
|
||||
form.Set("description", user.Description)
|
||||
form.Add("hooks", "pre_login_disabled")
|
||||
|
@ -17658,6 +17663,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.False(t, updateUser.Filters.Hooks.CheckPasswordDisabled)
|
||||
assert.False(t, updateUser.Filters.DisableFsChecks)
|
||||
assert.True(t, updateUser.Filters.AllowAPIKeyAuth)
|
||||
assert.Equal(t, 1, updateUser.Filters.FTPSecurity)
|
||||
// now check that a redacted password is not saved
|
||||
form.Set("s3_access_secret", redactedSecret)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -17756,6 +17762,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -17795,6 +17802,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
assert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, "*.png")
|
||||
assert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, "*.jpg")
|
||||
}
|
||||
assert.Equal(t, 1, updateUser.Filters.FTPSecurity)
|
||||
form.Set("gcs_auto_credentials", "on")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
|
|
@ -1279,6 +1279,9 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
if err != nil {
|
||||
return filters, fmt.Errorf("invalid max upload file size: %w", err)
|
||||
}
|
||||
if r.Form.Get("ftp_security") == "1" {
|
||||
filters.FTPSecurity = 1
|
||||
}
|
||||
filters.BandwidthLimits = bwLimits
|
||||
filters.DataTransferLimits = dtLimits
|
||||
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
|
|
|
@ -2066,6 +2066,9 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
|
|||
if expected.ExternalAuthCacheTime != actual.ExternalAuthCacheTime {
|
||||
return errors.New("external_auth_cache_time mismatch")
|
||||
}
|
||||
if expected.FTPSecurity != actual.FTPSecurity {
|
||||
return errors.New("ftp_security mismatch")
|
||||
}
|
||||
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4747,6 +4747,12 @@ components:
|
|||
items:
|
||||
$ref: '#/components/schemas/MFAProtocols'
|
||||
description: 'Defines protocols that require two factor authentication'
|
||||
ftp_security:
|
||||
type: integer
|
||||
enum:
|
||||
- 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.'
|
||||
description: Additional user options
|
||||
UserFilters:
|
||||
allOf:
|
||||
|
|
|
@ -690,6 +690,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idFTPSecurity" class="col-sm-2 col-form-label">FTP security</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idFTPSecurity" name="ftp_security" aria-describedby="ftpSecurityHelpBlock">
|
||||
<option value="" {{if eq .Group.UserSettings.Filters.FTPSecurity 0 }}selected{{end}}>Server settings</option>
|
||||
<option value="1" {{if eq .Group.UserSettings.Filters.FTPSecurity 1 }}selected{{end}}>Mandatory encryption</option>
|
||||
</select>
|
||||
<small id="ftpSecurityHelpBlock" class="form-text text-muted">
|
||||
Ignored if TLS is globally required for all FTP users
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -908,6 +908,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idFTPSecurity" class="col-sm-2 col-form-label">FTP security</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idFTPSecurity" name="ftp_security" aria-describedby="ftpSecurityHelpBlock">
|
||||
<option value="" {{if eq .User.Filters.FTPSecurity 0 }}selected{{end}}>Server settings</option>
|
||||
<option value="1" {{if eq .User.Filters.FTPSecurity 1 }}selected{{end}}>Mandatory encryption</option>
|
||||
</select>
|
||||
<small id="ftpSecurityHelpBlock" class="form-text text-muted">
|
||||
Ignored if TLS is globally required for all FTP users
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
Loading…
Reference in a new issue