mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
allow to require two-factor auth for users
Fixes #721 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
df828b6021
commit
d8de0faef5
21 changed files with 683 additions and 240 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
6
go.mod
|
@ -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
11
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -28,11 +28,13 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
claimUsernameKey = "username"
|
||||
claimPermissionsKey = "permissions"
|
||||
claimAPIKey = "api_key"
|
||||
basicRealm = "Basic realm=\"SFTPGo\""
|
||||
jwtCookieKey = "jwt"
|
||||
claimUsernameKey = "username"
|
||||
claimPermissionsKey = "permissions"
|
||||
claimAPIKey = "api_key"
|
||||
claimMustSetSecondFactorKey = "2fa_required"
|
||||
claimRequiredTwoFactorProtocols = "2fa_protocols"
|
||||
basicRealm = "Basic realm=\"SFTPGo\""
|
||||
jwtCookieKey = "jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -44,11 +46,13 @@ var (
|
|||
)
|
||||
|
||||
type jwtTokenClaims struct {
|
||||
Username string
|
||||
Permissions []string
|
||||
Signature string
|
||||
Audience string
|
||||
APIKeyID string
|
||||
Username string
|
||||
Permissions []string
|
||||
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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
129
httpd/server.go
129
httpd/server.go
|
@ -640,9 +640,11 @@ func (s *httpdServer) loginUser(
|
|||
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, error string),
|
||||
) {
|
||||
c := jwtTokenClaims{
|
||||
Username: user.Username,
|
||||
Permissions: user.Filters.WebClient,
|
||||
Signature: user.GetSignature(),
|
||||
Username: user.Username,
|
||||
Permissions: user.Filters.WebClient,
|
||||
Signature: user.GetSignature(),
|
||||
MustSetTwoFactorAuth: user.MustSetSecondFactor(),
|
||||
RequiredTwoFactorProtocols: user.Filters.TwoFactorAuthProtocols,
|
||||
}
|
||||
|
||||
audience := tokenAudienceWebClient
|
||||
|
@ -792,9 +794,11 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (s *httpdServer) generateAndSendUserToken(w http.ResponseWriter, r *http.Request, ipAddr string, user dataprovider.User) {
|
||||
c := jwtTokenClaims{
|
||||
Username: user.Username,
|
||||
Permissions: user.Filters.WebClient,
|
||||
Signature: user.GetSignature(),
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -153,19 +153,20 @@ type fsWrapper struct {
|
|||
|
||||
type userPage struct {
|
||||
basePage
|
||||
User *dataprovider.User
|
||||
RootPerms []string
|
||||
Error string
|
||||
ValidPerms []string
|
||||
ValidLoginMethods []string
|
||||
ValidProtocols []string
|
||||
WebClientOptions []string
|
||||
RootDirPerms []string
|
||||
RedactedSecret string
|
||||
Mode userPageMode
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
CanImpersonate bool
|
||||
FsWrapper fsWrapper
|
||||
User *dataprovider.User
|
||||
RootPerms []string
|
||||
Error string
|
||||
ValidPerms []string
|
||||
ValidLoginMethods []string
|
||||
ValidProtocols []string
|
||||
TwoFactorProtocols []string
|
||||
WebClientOptions []string
|
||||
RootDirPerms []string
|
||||
RedactedSecret string
|
||||
Mode userPageMode
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
CanImpersonate bool
|
||||
FsWrapper fsWrapper
|
||||
}
|
||||
|
||||
type adminPage struct {
|
||||
|
@ -606,17 +607,18 @@ func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.U
|
|||
}
|
||||
user.FsConfig.RedactedSecret = redactedSecret
|
||||
data := userPage{
|
||||
basePage: getBasePageData(title, currentURL, r),
|
||||
Mode: mode,
|
||||
Error: error,
|
||||
User: user,
|
||||
ValidPerms: dataprovider.ValidPerms,
|
||||
ValidLoginMethods: dataprovider.ValidLoginMethods,
|
||||
ValidProtocols: dataprovider.ValidProtocols,
|
||||
WebClientOptions: sdk.WebClientOptions,
|
||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||
VirtualFolders: folders,
|
||||
CanImpersonate: os.Getuid() == 0,
|
||||
basePage: getBasePageData(title, currentURL, r),
|
||||
Mode: mode,
|
||||
Error: error,
|
||||
User: user,
|
||||
ValidPerms: dataprovider.ValidPerms,
|
||||
ValidLoginMethods: dataprovider.ValidLoginMethods,
|
||||
ValidProtocols: dataprovider.ValidProtocols,
|
||||
TwoFactorProtocols: dataprovider.MFAProtocols,
|
||||
WebClientOptions: sdk.WebClientOptions,
|
||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||
VirtualFolders: folders,
|
||||
CanImpersonate: os.Getuid() == 0,
|
||||
FsWrapper: fsWrapper{
|
||||
Filesystem: user.FsConfig,
|
||||
IsUserPage: true,
|
||||
|
@ -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"]
|
||||
|
|
202
logger/logger.go
202
logger/logger.go
|
@ -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...),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue