allow to require two-factor auth for users

Fixes #721

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-03-06 16:57:13 +01:00
parent df828b6021
commit d8de0faef5
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
21 changed files with 683 additions and 240 deletions

View file

@ -119,9 +119,9 @@ var (
PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod,
PermChown, PermChtimes}
// ValidLoginMethods defines all the valid login methods
ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodKeyboardInteractive,
SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt, LoginMethodTLSCertificate,
LoginMethodTLSCertificateAndPwd}
ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodPassword,
SSHLoginMethodKeyboardInteractive, SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt,
LoginMethodTLSCertificate, LoginMethodTLSCertificateAndPwd}
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
// ErrNoAuthTryed defines the error for connection closed before authentication
@ -872,7 +872,7 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco
return err
}
if loginMethod == LoginMethodTLSCertificate {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return fmt.Errorf("certificate login method is not allowed for user %#v", user.User.Username)
}
return nil
@ -918,7 +918,7 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str
if err != nil {
return user, loginMethod, err
}
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol, nil) {
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
}
if loginMethod == LoginMethodTLSCertificateAndPwd {
@ -1805,7 +1805,6 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
return util.NewValidationError(fmt.Sprintf("totp: unable to encrypt secret: %v", err))
}
}
c.Protocols = util.RemoveDuplicates(c.Protocols)
if len(c.Protocols) == 0 {
return util.NewValidationError("totp: specify at least one protocol")
}
@ -1987,7 +1986,6 @@ func validateBandwidthLimit(bl sdk.BandwidthLimit) error {
func validateBandwidthLimitsFilter(user *User) error {
for idx, bandwidthLimit := range user.Filters.BandwidthLimits {
user.Filters.BandwidthLimits[idx].Sources = util.RemoveDuplicates(bandwidthLimit.Sources)
if err := validateBandwidthLimit(bandwidthLimit); err != nil {
return err
}
@ -2033,6 +2031,24 @@ func updateFiltersValues(user *User) {
}
}
func validateFilterProtocols(user *User) error {
if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) {
return util.NewValidationError("invalid denied_protocols")
}
for _, p := range user.Filters.DeniedProtocols {
if !util.IsStringInSlice(p, ValidProtocols) {
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %#v", p))
}
}
for _, p := range user.Filters.TwoFactorAuthProtocols {
if !util.IsStringInSlice(p, MFAProtocols) {
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %#v", p))
}
}
return nil
}
func validateFilters(user *User) error {
checkEmptyFiltersStruct(user)
if err := validateIPFilters(user); err != nil {
@ -2044,7 +2060,6 @@ func validateFilters(user *User) error {
if err := validateTransferLimitsFilter(user); err != nil {
return err
}
user.Filters.DeniedLoginMethods = util.RemoveDuplicates(user.Filters.DeniedLoginMethods)
if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return util.NewValidationError("invalid denied_login_methods")
}
@ -2053,21 +2068,14 @@ func validateFilters(user *User) error {
return util.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod))
}
}
user.Filters.DeniedProtocols = util.RemoveDuplicates(user.Filters.DeniedProtocols)
if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) {
return util.NewValidationError("invalid denied_protocols")
}
for _, p := range user.Filters.DeniedProtocols {
if !util.IsStringInSlice(p, ValidProtocols) {
return util.NewValidationError(fmt.Sprintf("invalid protocol: %#v", p))
}
if err := validateFilterProtocols(user); err != nil {
return err
}
if user.Filters.TLSUsername != "" {
if !util.IsStringInSlice(string(user.Filters.TLSUsername), validTLSUsernames) {
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername))
}
}
user.Filters.WebClient = util.RemoveDuplicates(user.Filters.WebClient)
for _, opts := range user.Filters.WebClient {
if !util.IsStringInSlice(opts, sdk.WebClientOptions) {
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
@ -2244,7 +2252,7 @@ func ValidateUser(user *User) error {
return err
}
if user.Filters.TOTPConfig.Enabled && util.IsStringInSlice(sdk.WebClientMFADisabled, user.Filters.WebClient) {
return util.NewValidationError("multi-factor authentication cannot be disabled for a user with an active configuration")
return util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration")
}
return saveGCSCredentials(&user.FsConfig, user)
}
@ -2405,7 +2413,7 @@ func checkUserAndPubKey(user *User, pubKey []byte) (User, string, error) {
certInfo = fmt.Sprintf(" %v ID: %v Serial: %v CA: %v", cert.Type(), cert.KeyId, cert.Serial,
ssh.FingerprintSHA256(cert.SignatureKey))
}
return *user, fmt.Sprintf("%v:%v%v", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil
return *user, fmt.Sprintf("%s:%s%s", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil
}
}
return *user, "", ErrInvalidCredentials

View file

@ -65,6 +65,7 @@ const (
const (
LoginMethodNoAuthTryed = "no_auth_tryed"
LoginMethodPassword = "password"
SSHLoginMethodPassword = "password-over-SSH"
SSHLoginMethodPublicKey = "publickey"
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
SSHLoginMethodKeyAndPassword = "publickey+password"
@ -827,7 +828,7 @@ func (u *User) HasNoQuotaRestrictions(checkFiles bool) bool {
}
// IsLoginMethodAllowed returns true if the specified login method is allowed
func (u *User) IsLoginMethodAllowed(loginMethod string, partialSuccessMethods []string) bool {
func (u *User) IsLoginMethodAllowed(loginMethod, protocol string, partialSuccessMethods []string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 {
return true
}
@ -841,6 +842,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod string, partialSuccessMethods []
if util.IsStringInSlice(loginMethod, u.Filters.DeniedLoginMethods) {
return false
}
if protocol == protocolSSH && loginMethod == LoginMethodPassword {
if util.IsStringInSlice(SSHLoginMethodPassword, u.Filters.DeniedLoginMethods) {
return false
}
}
return true
}
@ -875,7 +881,8 @@ func (u *User) IsPartialAuth(loginMethod string) bool {
return false
}
for _, method := range u.GetAllowedLoginMethods() {
if method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd {
if method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd ||
method == SSHLoginMethodPassword {
continue
}
if !util.IsStringInSlice(method, SSHMultiStepsLoginMethods) {
@ -889,6 +896,9 @@ func (u *User) IsPartialAuth(loginMethod string) bool {
func (u *User) GetAllowedLoginMethods() []string {
var allowedMethods []string
for _, method := range ValidLoginMethods {
if method == SSHLoginMethodPassword {
continue
}
if !util.IsStringInSlice(method, u.Filters.DeniedLoginMethods) {
allowedMethods = append(allowedMethods, method)
}
@ -1055,6 +1065,35 @@ func (u *User) CanDeleteFromWeb(target string) bool {
return u.HasAnyPerm(permsDeleteAny, target)
}
// MustSetSecondFactor returns true if the user must set a second factor authentication
func (u *User) MustSetSecondFactor() bool {
if len(u.Filters.TwoFactorAuthProtocols) > 0 {
if !u.Filters.TOTPConfig.Enabled {
return true
}
for _, p := range u.Filters.TwoFactorAuthProtocols {
if !util.IsStringInSlice(p, u.Filters.TOTPConfig.Protocols) {
return true
}
}
}
return false
}
// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
// for the specified protocol
func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
if util.IsStringInSlice(protocol, u.Filters.TwoFactorAuthProtocols) {
if !u.Filters.TOTPConfig.Enabled {
return true
}
if !util.IsStringInSlice(protocol, u.Filters.TOTPConfig.Protocols) {
return true
}
}
return false
}
// GetSignature returns a signature for this admin.
// It could change after an update
func (u *User) GetSignature() string {
@ -1437,6 +1476,8 @@ 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.TwoFactorAuthProtocols = make([]string, len(u.Filters.TwoFactorAuthProtocols))
copy(filters.TwoFactorAuthProtocols, u.Filters.TwoFactorAuthProtocols)
filters.Hooks.ExternalAuthDisabled = u.Filters.Hooks.ExternalAuthDisabled
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled

View file

@ -703,6 +703,44 @@ func TestMultiFactorAuth(t *testing.T) {
assert.NoError(t, err)
}
func TestSecondFactorRequirement(t *testing.T) {
u := getTestUser()
u.Filters.TwoFactorAuthProtocols = []string{common.ProtocolFTP}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, err = getFTPClient(user, true, nil)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "second factor authentication is not set")
}
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Protocols: []string{common.ProtocolFTP},
}
err = dataprovider.UpdateUser(&user, "", "")
assert.NoError(t, err)
passcode, err := generateTOTPPasscode(secret, otp.AlgorithmSHA1)
assert.NoError(t, err)
user.Password = defaultPassword + passcode
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)
}
func TestLoginInvalidCredentials(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)

View file

@ -239,7 +239,7 @@ func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsCo
s.setTLSConnVerified(cc.ID(), true)
if dbUser.IsLoginMethodAllowed(dataprovider.LoginMethodTLSCertificate, nil) {
if dbUser.IsLoginMethodAllowed(dataprovider.LoginMethodTLSCertificate, common.ProtocolFTP, nil) {
connection, err := s.validateUser(dbUser, cc, dataprovider.LoginMethodTLSCertificate)
defer updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err)
@ -330,11 +330,16 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
logger.Info(logSender, connectionID, "cannot login user %#v, protocol FTP is not allowed", user.Username)
return nil, fmt.Errorf("protocol FTP is not allowed for user %#v", user.Username)
}
if !user.IsLoginMethodAllowed(loginMethod, nil) {
if !user.IsLoginMethodAllowed(loginMethod, common.ProtocolFTP, nil) {
logger.Info(logSender, connectionID, "cannot login user %#v, %v login method is not allowed",
user.Username, loginMethod)
return nil, fmt.Errorf("login method %v is not allowed for user %#v", loginMethod, user.Username)
}
if user.MustSetSecondFactorForProtocol(common.ProtocolFTP) {
logger.Info(logSender, connectionID, "cannot login user %#v, second factor authentication is not set",
user.Username)
return nil, fmt.Errorf("second factor authentication is not set for user %#v", user.Username)
}
if user.MaxSessions > 0 {
activeSessions := common.Connections.GetActiveSessions(user.Username)
if activeSessions >= user.MaxSessions {

6
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go v1.43.11
github.com/aws/aws-sdk-go v1.43.12
github.com/cockroachdb/cockroach-go/v2 v2.2.8
github.com/coreos/go-oidc/v3 v3.1.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
@ -41,7 +41,7 @@ require (
github.com/rs/cors v1.8.2
github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672
github.com/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712
github.com/sftpgo/sdk v0.1.1-0.20220306155429-3a036106d884
github.com/shirou/gopsutil/v3 v3.22.2
github.com/spf13/afero v1.8.1
github.com/spf13/cobra v1.3.0
@ -98,7 +98,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.0 // indirect
github.com/lestrrat-go/iter v1.0.1 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lufia/plan9stats v0.0.0-20220305071607-d0b38dbe16db // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect

11
go.sum
View file

@ -145,8 +145,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.43.11 h1:NebCNJ2QvsFCnsKT1ei98bfwTPEoO2qwtWT42tJ3N3Q=
github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.43.12 h1:wOdx6+reSDpUBFEuJDA6edCrojzy8rOtMzhS2rD9+7M=
github.com/aws/aws-sdk-go v1.43.12/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
@ -562,8 +562,9 @@ github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20220305071607-d0b38dbe16db h1:QT3DrSQsMWGKZMArbkP9FlS2ZnPLA2z8D7fU+G3BZ3o=
github.com/lufia/plan9stats v0.0.0-20220305071607-d0b38dbe16db/go.mod h1:VgrrWVwBO2+6XKn8ypT3WUqvoxCa8R2M5to2tRzGovI=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
@ -702,8 +703,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/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712 h1:+Rgx0SgsDnFSI5JBwL4mcCH2lkx3yKhLWcQnf0s2JKE=
github.com/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
github.com/sftpgo/sdk v0.1.1-0.20220306155429-3a036106d884 h1:YrOexWq3hwNk/QM3ZyP/VI2E7UcCj/PMqJd1PLA1EME=
github.com/sftpgo/sdk v0.1.1-0.20220306155429-3a036106d884/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=

View file

@ -90,6 +90,13 @@ func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
if claims.MustSetTwoFactorAuth {
// force logout
defer func() {
c := jwtTokenClaims{}
c.removeCookie(w, r, webBaseClientPath)
}()
}
} else {
if err := saveAdminTOTPConfig(claims.Username, r, recoveryCodes); err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
@ -210,6 +217,15 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []datapr
if err != nil {
return util.NewValidationError(fmt.Sprintf("unable to decode JSON body: %v", err))
}
if !user.Filters.TOTPConfig.Enabled && len(user.Filters.TwoFactorAuthProtocols) > 0 {
return util.NewValidationError("two-factor authentication must be enabled")
}
for _, p := range user.Filters.TwoFactorAuthProtocols {
if !util.IsStringInSlice(p, user.Filters.TOTPConfig.Protocols) {
return util.NewValidationError(fmt.Sprintf("totp: the following protocols are required: %#v",
strings.Join(user.Filters.TwoFactorAuthProtocols, ", ")))
}
}
if user.Filters.TOTPConfig.Secret == nil || !user.Filters.TOTPConfig.Secret.IsPlain() {
user.Filters.TOTPConfig.Secret = currentTOTPSecret
}

View file

@ -396,6 +396,11 @@ func checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope datapro
renderError(err, "", getRespStatus(err))
return share, nil, err
}
if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) {
err := util.NewMethodDisabledError("two-factor authentication requirements not met")
renderError(err, "", getRespStatus(err))
return share, nil, err
}
connID := xid.New().String()
connection := &Connection{
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r),

View file

@ -503,7 +503,7 @@ func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID
logger.Info(logSender, connectionID, "cannot login user %#v, protocol HTTP is not allowed", user.Username)
return fmt.Errorf("protocol HTTP is not allowed for user %#v", user.Username)
}
if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP, nil) {
logger.Info(logSender, connectionID, "cannot login user %#v, password login method is not allowed", user.Username)
return fmt.Errorf("login method password is not allowed for user %#v", user.Username)
}
@ -634,7 +634,7 @@ func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool
if util.IsStringInSlice(common.ProtocolHTTP, user.Filters.DeniedProtocols) {
return false
}
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP, nil) {
return false
}
if !user.IsLoginFromAddrAllowed(r.RemoteAddr) {

View file

@ -31,6 +31,8 @@ const (
claimUsernameKey = "username"
claimPermissionsKey = "permissions"
claimAPIKey = "api_key"
claimMustSetSecondFactorKey = "2fa_required"
claimRequiredTwoFactorProtocols = "2fa_protocols"
basicRealm = "Basic realm=\"SFTPGo\""
jwtCookieKey = "jwt"
)
@ -49,6 +51,8 @@ type jwtTokenClaims struct {
Signature string
Audience string
APIKeyID string
MustSetTwoFactorAuth bool
RequiredTwoFactorProtocols []string
}
func (c *jwtTokenClaims) hasUserAudience() bool {
@ -67,6 +71,8 @@ func (c *jwtTokenClaims) asMap() map[string]interface{} {
claims[claimAPIKey] = c.APIKeyID
}
claims[jwt.SubjectKey] = c.Signature
claims[claimMustSetSecondFactorKey] = c.MustSetTwoFactorAuth
claims[claimRequiredTwoFactorProtocols] = c.RequiredTwoFactorProtocols
return claims
}
@ -113,6 +119,23 @@ func (c *jwtTokenClaims) Decode(token map[string]interface{}) {
}
}
}
secondFactorRequired := token[claimMustSetSecondFactorKey]
switch v := secondFactorRequired.(type) {
case bool:
c.MustSetTwoFactorAuth = v
}
secondFactorProtocols := token[claimRequiredTwoFactorProtocols]
switch v := secondFactorProtocols.(type) {
case []interface{}:
for _, elem := range v {
switch elemValue := elem.(type) {
case string:
c.RequiredTwoFactorProtocols = append(c.RequiredTwoFactorProtocols, elemValue)
}
}
}
}
func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {

View file

@ -989,7 +989,7 @@ func TestPermMFADisabled(t *testing.T) {
user.Filters.WebClient = []string{sdk.WebClientMFADisabled}
_, resp, err := httpdtest.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
assert.Contains(t, string(resp), "multi-factor authentication cannot be disabled for a user with an active configuration")
assert.Contains(t, string(resp), "two-factor authentication cannot be disabled for a user with an active configuration")
saveReq := make(map[string]bool)
saveReq["enabled"] = false
@ -1027,6 +1027,90 @@ func TestPermMFADisabled(t *testing.T) {
assert.NoError(t, err)
}
func TestTwoFactorRequirements(t *testing.T) {
u := getTestUser()
u.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolFTP}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols")
req, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
assert.NoError(t, err)
req.RequestURI = webClientFilesPath
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols")
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
userTOTPConfig := dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Protocols: []string{common.ProtocolHTTP},
}
asJSON, err := json.Marshal(userTOTPConfig)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "the following protocols are required")
userTOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolFTP}
asJSON, err = json.Marshal(userTOTPConfig)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
// now get new tokens and check that the two factor requirements are now met
passcode, err := generateTOTPPasscode(secret)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
req.Header.Set("X-SFTPGO-OTP", passcode)
req.SetBasicAuth(defaultUsername, defaultPassword)
resp, err := httpclient.GetHTTPClient().Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
responseHolder := make(map[string]interface{})
err = render.DecodeJSON(resp.Body, &responseHolder)
assert.NoError(t, err)
userToken := responseHolder["access_token"].(string)
assert.NotEmpty(t, userToken)
err = resp.Body.Close()
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userDirsPath), nil)
assert.NoError(t, err)
setBearerForReq(req, userToken)
resp, err = httpclient.GetHTTPClient().Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = resp.Body.Close()
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestLoginUserAPITOTP(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -1048,6 +1132,39 @@ func TestLoginUserAPITOTP(t *testing.T) {
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
// now require HTTP and SSH for TOTP
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
// two factor auth cannot be disabled
config := make(map[string]interface{})
config["enabled"] = false
asJSON, err = json.Marshal(config)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "two-factor authentication must be enabled")
// all the required protocols must be enabled
asJSON, err = json.Marshal(userTOTPConfig)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "the following protocols are required")
// setting all the required protocols should work
userTOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolSSH}
asJSON, err = json.Marshal(userTOTPConfig)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, userTokenPath), nil)
assert.NoError(t, err)
@ -1070,8 +1187,8 @@ func TestLoginUserAPITOTP(t *testing.T) {
responseHolder := make(map[string]interface{})
err = render.DecodeJSON(resp.Body, &responseHolder)
assert.NoError(t, err)
adminToken := responseHolder["access_token"].(string)
assert.NotEmpty(t, adminToken)
userToken := responseHolder["access_token"].(string)
assert.NotEmpty(t, userToken)
err = resp.Body.Close()
assert.NoError(t, err)
@ -1543,7 +1660,11 @@ func TestAddUserInvalidFilters(t *testing.T) {
u.Filters.DeniedLoginMethods = dataprovider.ValidLoginMethods
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err)
u.Filters.DeniedLoginMethods = []string{}
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}
u.Filters.DeniedProtocols = dataprovider.ValidProtocols
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err)
u.Filters.DeniedProtocols = []string{common.ProtocolFTP}
u.Filters.FilePatterns = []sdk.PatternsFilter{
{
Path: "relative",
@ -9949,6 +10070,22 @@ func TestBrowseShares(t *testing.T) {
err = json.Unmarshal(rr.Body.Bytes(), &contents)
assert.NoError(t, err)
assert.Len(t, contents, 1)
// if we require two-factor auth for HTTP protocol the share should not work anymore
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "dirs?path=%2F"), nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH, common.ProtocolHTTP}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "dirs?path=%2F"), nil)
assert.NoError(t, err)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "two-factor authentication requirements not met")
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -14560,7 +14697,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("pattern_path0", "/dir1")
form.Set("patterns0", "*.zip")
form.Set("pattern_type0", "denied")
form.Set("ssh_login_methods", dataprovider.SSHLoginMethodKeyboardInteractive)
form.Set("denied_login_methods", dataprovider.SSHLoginMethodKeyboardInteractive)
form.Set("denied_protocols", common.ProtocolFTP)
form.Set("max_upload_file_size", "100")
form.Set("disconnect", "1")
@ -17005,6 +17142,33 @@ func TestStaticFilesMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
}
func TestSecondFactorRequirements(t *testing.T) {
user := getTestUser()
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}
assert.True(t, user.MustSetSecondFactor())
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))
assert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))
assert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))
user.Filters.TOTPConfig.Enabled = true
assert.True(t, user.MustSetSecondFactor())
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))
assert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))
assert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))
user.Filters.TOTPConfig.Protocols = []string{common.ProtocolHTTP}
assert.True(t, user.MustSetSecondFactor())
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))
assert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))
user.Filters.TOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolSSH}
assert.False(t, user.MustSetSecondFactor())
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))
assert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))
}
func startOIDCMockServer() {
go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

View file

@ -1002,6 +1002,21 @@ func TestJWTTokenValidation(t *testing.T) {
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
fn = checkSecondFactorRequirement(r)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
req.RequestURI = webClientProfilePath
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, userSharesPath, nil)
req.RequestURI = userSharesPath
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestUpdateContextFromCookie(t *testing.T) {

View file

@ -198,6 +198,34 @@ func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
}
}
func checkSecondFactorRequirement(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
if isWebRequest(r) {
renderClientBadRequestPage(w, r, err)
} else {
sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
return
}
tokenClaims := jwtTokenClaims{}
tokenClaims.Decode(claims)
if tokenClaims.MustSetTwoFactorAuth {
message := fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))
if isWebRequest(r) {
renderClientForbiddenPage(w, r, message)
} else {
sendAPIResponse(w, r, nil, message, http.StatusForbidden)
}
return
}
next.ServeHTTP(w, r)
})
}
func requireBuiltinLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isLoggedInWithOIDC(r) {

View file

@ -643,6 +643,8 @@ func (s *httpdServer) loginUser(
Username: user.Username,
Permissions: user.Filters.WebClient,
Signature: user.GetSignature(),
MustSetTwoFactorAuth: user.MustSetSecondFactor(),
RequiredTwoFactorProtocols: user.Filters.TwoFactorAuthProtocols,
}
audience := tokenAudienceWebClient
@ -795,6 +797,8 @@ func (s *httpdServer) generateAndSendUserToken(w http.ResponseWriter, r *http.Re
Username: user.Username,
Permissions: user.Filters.WebClient,
Signature: user.GetSignature(),
MustSetTwoFactorAuth: user.MustSetSecondFactor(),
RequiredTwoFactorProtocols: user.Filters.TwoFactorAuthProtocols,
}
resp, err := c.createTokenResponse(s.tokenAuth, tokenAudienceAPIUser)
@ -1241,14 +1245,14 @@ func (s *httpdServer) initializeRouter() {
router.Use(jwtAuthenticatorAPIUser)
router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
Put(userPwdPath, changeUserPassword)
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
Get(userPublicKeysPath, getUserPublicKeys)
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
Put(userPublicKeysPath, setUserPublicKeys)
router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).Put(userPwdPath, changeUserPassword)
router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
router.With(forbidAPIKeyAuthentication).Put(userProfilePath, updateUserProfile)
router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement).Put(userProfilePath, updateUserProfile)
// user TOTP APIs
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
Get(userTOTPConfigsPath, getTOTPConfigs)
@ -1264,25 +1268,38 @@ func (s *httpdServer) initializeRouter() {
Post(user2FARecoveryCodesPath, generateRecoveryCodes)
// compatibility layer to remove in v2.3
router.With(compressor.Handler).Get(userFolderPath, readUserFolder)
router.Get(userFilePath, getUserFile)
router.With(checkSecondFactorRequirement, compressor.Handler).Get(userFolderPath, readUserFolder)
router.With(checkSecondFactorRequirement).Get(userFilePath, getUserFile)
router.With(compressor.Handler).Get(userDirsPath, readUserFolder)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userDirsPath, createUserDir)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userDirsPath, renameUserDir)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userDirsPath, deleteUserDir)
router.Get(userFilesPath, getUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFilesPath, uploadUserFiles)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilesPath, renameUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFilesPath, deleteUserFile)
router.Post(userStreamZipPath, getUserFilesAsZipStream)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Get(userSharesPath, getShares)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Post(userSharesPath, addShare)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Get(userSharesPath+"/{id}", getShareByID)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Put(userSharesPath+"/{id}", updateShare)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Delete(userSharesPath+"/{id}", deleteShare)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userUploadFilePath, uploadUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilesDirsMetadataPath, setFileDirMetadata)
router.With(checkSecondFactorRequirement, compressor.Handler).Get(userDirsPath, readUserFolder)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Post(userDirsPath, createUserDir)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Patch(userDirsPath, renameUserDir)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Delete(userDirsPath, deleteUserDir)
router.With(checkSecondFactorRequirement).Get(userFilesPath, getUserFile)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Post(userFilesPath, uploadUserFiles)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Patch(userFilesPath, renameUserFile)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Delete(userFilesPath, deleteUserFile)
router.With(checkSecondFactorRequirement).Post(userStreamZipPath, getUserFilesAsZipStream)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Get(userSharesPath, getShares)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Post(userSharesPath, addShare)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Get(userSharesPath+"/{id}", getShareByID)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Put(userSharesPath+"/{id}", updateShare)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Delete(userSharesPath+"/{id}", deleteShare)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Post(userUploadFilePath, uploadUserFile)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
Patch(userFilesDirsMetadataPath, setFileDirMetadata)
})
if s.renderOpenAPI {
@ -1368,29 +1385,33 @@ func (s *httpdServer) setupWebClientRoutes() {
router.Use(jwtAuthenticatorWebClient)
router.Get(webClientLogoutPath, s.handleWebClientLogout)
router.With(s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
router.With(s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
router.With(checkSecondFactorRequirement, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Post(webClientFilePath, uploadUserFile)
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Patch(webClientFilesPath, renameUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Delete(webClientFilesPath, deleteUserFile)
router.With(compressor.Handler, s.refreshCookie).Get(webClientDirsPath, s.handleClientGetDirContents)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, compressor.Handler, s.refreshCookie).
Get(webClientDirsPath, s.handleClientGetDirContents)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Post(webClientDirsPath, createUserDir)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Patch(webClientDirsPath, renameUserDir)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Delete(webClientDirsPath, deleteUserDir)
router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip)
router.With(s.refreshCookie, requireBuiltinLogin).Get(webClientProfilePath, handleClientGetProfile)
router.With(requireBuiltinLogin).Post(webClientProfilePath, handleWebClientProfilePost)
router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
router.With(checkSecondFactorRequirement, s.refreshCookie).
Get(webClientDownloadZipPath, handleWebClientDownloadZip)
router.With(checkSecondFactorRequirement, s.refreshCookie, requireBuiltinLogin).
Get(webClientProfilePath, handleClientGetProfile)
router.With(checkSecondFactorRequirement, requireBuiltinLogin).
Post(webClientProfilePath, handleWebClientProfilePost)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
Get(webChangeClientPwdPath, handleWebClientChangePwd)
router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
Post(webChangeClientPwdPath, s.handleWebClientChangePwdPost)
router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
Get(webClientMFAPath, handleWebClientMFA)
@ -1404,17 +1425,17 @@ func (s *httpdServer) setupWebClientRoutes() {
Get(webClientRecoveryCodesPath, getRecoveryCodes)
router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
Post(webClientRecoveryCodesPath, generateRecoveryCodes)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
Get(webClientSharesPath, handleClientGetShares)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
Get(webClientSharePath, handleClientAddShareGet)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Post(webClientSharePath,
handleClientAddSharePost)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Post(webClientSharePath, handleClientAddSharePost)
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
Get(webClientSharePath+"/{id}", handleClientUpdateShareGet)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
Post(webClientSharePath+"/{id}", handleClientUpdateSharePost)
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled), verifyCSRFHeader).
router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), verifyCSRFHeader).
Delete(webClientSharePath+"/{id}", deleteShare)
})
}

View file

@ -159,6 +159,7 @@ type userPage struct {
ValidPerms []string
ValidLoginMethods []string
ValidProtocols []string
TwoFactorProtocols []string
WebClientOptions []string
RootDirPerms []string
RedactedSecret string
@ -613,6 +614,7 @@ func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.U
ValidPerms: dataprovider.ValidPerms,
ValidLoginMethods: dataprovider.ValidLoginMethods,
ValidProtocols: dataprovider.ValidProtocols,
TwoFactorProtocols: dataprovider.MFAProtocols,
WebClientOptions: sdk.WebClientOptions,
RootDirPerms: user.GetPermissionsForPath("/"),
VirtualFolders: folders,
@ -930,8 +932,9 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
filters.DataTransferLimits = dtLimits
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
filters.DeniedLoginMethods = r.Form["denied_login_methods"]
filters.DeniedProtocols = r.Form["denied_protocols"]
filters.TwoFactorAuthProtocols = r.Form["required_two_factor_protocols"]
filters.FilePatterns = getFilePatternsFromPostField(r)
filters.TLSUsername = sdk.TLSUsername(r.Form.Get("tls_username"))
filters.WebClient = r.Form["web_client_options"]

View file

@ -41,116 +41,15 @@ var (
rollingLogger *lumberjack.Logger
)
// StdLoggerWrapper is a wrapper for standard logger compatibility
type StdLoggerWrapper struct {
Sender string
}
func init() {
zerolog.TimeFieldFormat = dateFormat
}
// Write implements the io.Writer interface. This is useful to set as a writer
// for the standard library log.
func (l *StdLoggerWrapper) Write(p []byte) (n int, err error) {
n = len(p)
if n > 0 && p[n-1] == '\n' {
// Trim CR added by stdlog.
p = p[0 : n-1]
}
Log(LevelError, l.Sender, "", string(p))
return
}
// LeveledLogger is a logger that accepts a message string and a variadic number of key-value pairs
type LeveledLogger struct {
Sender string
additionalKeyVals []interface{}
}
func addKeysAndValues(ev *zerolog.Event, keysAndValues ...interface{}) {
kvLen := len(keysAndValues)
if kvLen%2 != 0 {
extra := keysAndValues[kvLen-1]
keysAndValues = append(keysAndValues[:kvLen-1], "EXTRA_VALUE_AT_END", extra)
}
for i := 0; i < len(keysAndValues); i += 2 {
key, val := keysAndValues[i], keysAndValues[i+1]
if keyStr, ok := key.(string); ok && keyStr != "timestamp" {
ev.Str(keyStr, fmt.Sprintf("%v", val))
}
}
}
// Error logs at error level for the specified sender
func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
ev := logger.Error()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Info logs at info level for the specified sender
func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
ev := logger.Info()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Debug logs at debug level for the specified sender
func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
ev := logger.Debug()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Warn logs at warn level for the specified sender
func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
ev := logger.Warn()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// With returns a LeveledLogger with additional context specific keyvals
func (l *LeveledLogger) With(keysAndValues ...interface{}) ftpserverlog.Logger {
return &LeveledLogger{
Sender: l.Sender,
additionalKeyVals: append(l.additionalKeyVals, keysAndValues...),
}
}
// GetLogger get the configured logger instance
func GetLogger() *zerolog.Logger {
return &logger
}
// SetLogTime sets logging time related setting
func SetLogTime(utc bool) {
if utc {
zerolog.TimestampFunc = func() time.Time {
return time.Now().UTC()
}
} else {
zerolog.TimestampFunc = time.Now
}
}
// InitLogger configures the logger using the given parameters
func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge int, logCompress, logUTCTime bool,
level zerolog.Level,
@ -215,6 +114,17 @@ func RotateLogFile() error {
return errors.New("logging to file is disabled")
}
// SetLogTime sets logging time related setting
func SetLogTime(utc bool) {
if utc {
zerolog.TimestampFunc = func() time.Time {
return time.Now().UTC()
}
} else {
zerolog.TimestampFunc = time.Now
}
}
// Log logs at the specified level for the specified sender
func Log(level LogLevel, sender string, connectionID string, format string, v ...interface{}) {
var ev *zerolog.Event
@ -341,3 +251,93 @@ func isLogFilePathValid(logFilePath string) bool {
}
return true
}
// StdLoggerWrapper is a wrapper for standard logger compatibility
type StdLoggerWrapper struct {
Sender string
}
// Write implements the io.Writer interface. This is useful to set as a writer
// for the standard library log.
func (l *StdLoggerWrapper) Write(p []byte) (n int, err error) {
n = len(p)
if n > 0 && p[n-1] == '\n' {
// Trim CR added by stdlog.
p = p[0 : n-1]
}
Log(LevelError, l.Sender, "", string(p))
return
}
// LeveledLogger is a logger that accepts a message string and a variadic number of key-value pairs
type LeveledLogger struct {
Sender string
additionalKeyVals []interface{}
}
func addKeysAndValues(ev *zerolog.Event, keysAndValues ...interface{}) {
kvLen := len(keysAndValues)
if kvLen%2 != 0 {
extra := keysAndValues[kvLen-1]
keysAndValues = append(keysAndValues[:kvLen-1], "EXTRA_VALUE_AT_END", extra)
}
for i := 0; i < len(keysAndValues); i += 2 {
key, val := keysAndValues[i], keysAndValues[i+1]
if keyStr, ok := key.(string); ok && keyStr != "timestamp" {
ev.Str(keyStr, fmt.Sprintf("%v", val))
}
}
}
// Error logs at error level for the specified sender
func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
ev := logger.Error()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Info logs at info level for the specified sender
func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
ev := logger.Info()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Debug logs at debug level for the specified sender
func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
ev := logger.Debug()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// Warn logs at warn level for the specified sender
func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
ev := logger.Warn()
ev.Timestamp().Str("sender", l.Sender)
if len(l.additionalKeyVals) > 0 {
addKeysAndValues(ev, l.additionalKeyVals...)
}
addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg)
}
// With returns a LeveledLogger with additional context specific keyvals
func (l *LeveledLogger) With(keysAndValues ...interface{}) ftpserverlog.Logger {
return &LeveledLogger{
Sender: l.Sender,
additionalKeyVals: append(l.additionalKeyVals, keysAndValues...),
}
}

View file

@ -4308,6 +4308,7 @@ components:
enum:
- publickey
- password
- password-over-SSH
- keyboard-interactive
- publickey+password
- publickey+keyboard-interactive
@ -4316,7 +4317,8 @@ components:
description: |
Available login methods. To enable multi-step authentication you have to allow only multi-step login methods
* `publickey`
* `password`
* `password`, password for all the supported protocols
* `password-over-SSH`, password over SSH protocol (SSH/SFTP/SCP)
* `keyboard-interactive`
* `publickey+password` - multi-step auth: public key and password
* `publickey+keyboard-interactive` - multi-step auth: public key and keyboard interactive
@ -4682,6 +4684,11 @@ components:
start_directory:
type: string
description: 'Specifies an alternate starting directory. If not set, the default is "/". This option is supported for SFTP/SCP, FTP and HTTP (WebClient/REST API) protocols. Relative paths will use this directory as base.'
2fa_protocols:
type: array
items:
$ref: '#/components/schemas/MFAProtocols'
description: 'Defines protocols that require two factor authentication'
description: Additional user options
Secret:
type: object

View file

@ -632,11 +632,16 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
}
}
if !user.IsLoginMethodAllowed(loginMethod, conn.PartialSuccessMethods()) {
if !user.IsLoginMethodAllowed(loginMethod, common.ProtocolSSH, conn.PartialSuccessMethods()) {
logger.Info(logSender, connectionID, "cannot login user %#v, login method %#v is not allowed",
user.Username, loginMethod)
return nil, fmt.Errorf("login method %#v is not allowed for user %#v", loginMethod, user.Username)
}
if user.MustSetSecondFactorForProtocol(common.ProtocolSSH) {
logger.Info(logSender, connectionID, "cannot login user %#v, second factor authentication is not set",
user.Username)
return nil, fmt.Errorf("second factor authentication is not set for user %#v", user.Username)
}
remoteAddr := conn.RemoteAddr().String()
if !user.IsLoginFromAddrAllowed(remoteAddr) {
logger.Info(logSender, connectionID, "cannot login user %#v, remote address is not allowed: %v",
@ -649,7 +654,7 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
logger.Warn(logSender, connectionID, "error serializing user info: %v, authentication rejected", err)
return nil, err
}
if len(publicKey) > 0 {
if publicKey != "" {
loginMethod = fmt.Sprintf("%v: %v", loginMethod, publicKey)
}
p := &ssh.Permissions{}

View file

@ -2497,6 +2497,41 @@ func TestInteractiveLoginWithPasscode(t *testing.T) {
assert.NoError(t, err)
}
func TestSecondFactorRequirement(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
u.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, _, err = getSftpClient(user, usePubKey)
assert.Error(t, err)
configName, _, secret, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err)
user.Password = defaultPassword
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: true,
ConfigName: configName,
Secret: kms.NewPlainSecret(secret),
Protocols: []string{common.ProtocolSSH},
}
err = dataprovider.UpdateUser(&user, "", "")
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestNamingRules(t *testing.T) {
err := dataprovider.Close()
assert.NoError(t, err)
@ -7830,18 +7865,31 @@ func TestUserIsLoginMethodAllowed(t *testing.T) {
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPublicKey, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, nil))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, []string{dataprovider.SSHLoginMethodPublicKey}))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, []string{dataprovider.SSHLoginMethodPublicKey}))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyAndPassword, []string{dataprovider.SSHLoginMethodPublicKey}))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolFTP, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolWebDAV, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPublicKey, common.ProtocolSSH, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, common.ProtocolSSH, nil))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH,
[]string{dataprovider.SSHLoginMethodPublicKey}))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, common.ProtocolSSH,
[]string{dataprovider.SSHLoginMethodPublicKey}))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyAndPassword, common.ProtocolSSH,
[]string{dataprovider.SSHLoginMethodPublicKey}))
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH, nil))
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
}
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP, nil))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolFTP, nil))
assert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolWebDAV, nil))
assert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH, nil))
}
func TestUserEmptySubDirPerms(t *testing.T) {

View file

@ -482,12 +482,27 @@
<div class="form-group row">
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
<div class="col-sm-10">
<select class="form-control" id="idLoginMethods" name="ssh_login_methods" multiple>
<select class="form-control" id="idLoginMethods" name="denied_login_methods" multiple aria-describedby="deniedLoginMethodsHelpBlock">
{{range $method := .ValidLoginMethods}}
<option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
</option>
{{end}}
</select>
<small id="deniedLoginMethodsHelpBlock" class="form-text text-muted">
"password" is valid for all supported protocols, "password-over-SSH" only for SSH/SFTP/SCP
</small>
</div>
</div>
<div class="form-group row">
<label for="idTwoFactorProtocols" class="col-sm-2 col-form-label">Require two-factor auth for</label>
<div class="col-sm-10">
<select class="form-control" id="idTwoFactorProtocols" name="required_two_factor_protocols" multiple>
{{range $protocol := .TwoFactorProtocols}}
<option value="{{$protocol}}" {{range $p :=$.User.Filters.TwoFactorAuthProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
</option>
{{end}}
</select>
</div>
</div>

View file

@ -306,7 +306,7 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
logger.Info(logSender, connectionID, "cannot login user %#v, protocol DAV is not allowed", user.Username)
return connID, fmt.Errorf("protocol DAV is not allowed for user %#v", user.Username)
}
if !user.IsLoginMethodAllowed(loginMethod, nil) {
if !user.IsLoginMethodAllowed(loginMethod, common.ProtocolWebDAV, nil) {
logger.Info(logSender, connectionID, "cannot login user %#v, %v login method is not allowed",
user.Username, loginMethod)
return connID, fmt.Errorf("login method %v is not allowed for user %#v", loginMethod, user.Username)