add support for partial authentication

Multi-step authentication is activated disabling all single-step
auth methods for a given user
This commit is contained in:
Nicola Murino 2020-04-09 23:32:42 +02:00
parent a0fe4cf5e4
commit b1c7317cf6
20 changed files with 498 additions and 75 deletions

View file

@ -11,6 +11,7 @@ Fully featured and highly configurable SFTP server, written in Go
- SQLite, MySQL, PostgreSQL, bbolt (key/value store in pure Go) and in-memory data providers are supported. - SQLite, MySQL, PostgreSQL, bbolt (key/value store in pure Go) and in-memory data providers are supported.
- Public key and password authentication. Multiple public keys per user are supported. - Public key and password authentication. Multiple public keys per user are supported.
- Keyboard interactive authentication. You can easily setup a customizable multi-factor authentication. - Keyboard interactive authentication. You can easily setup a customizable multi-factor authentication.
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
- Per user authentication methods. You can, for example, deny one or more authentication methods to one or more users. - Per user authentication methods. You can, for example, deny one or more authentication methods to one or more users.
- Custom authentication via external programs is supported. - Custom authentication via external programs is supported.
- Dynamic user modification before login via external programs is supported. - Dynamic user modification before login via external programs is supported.

View file

@ -116,7 +116,7 @@ func (p BoltProvider) validateUserAndPass(username string, password string) (Use
return checkUserAndPass(user, password) return checkUserAndPass(user, password)
} }
func (p BoltProvider) validateUserAndPubKey(username string, pubKey string) (User, string, error) { func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
var user User var user User
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") return user, "", errors.New("Credentials cannot be null or empty")

View file

@ -73,18 +73,21 @@ const (
) )
var ( var (
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings // SupportedProviders defines the supported data providers
SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,
BoltDataProviderName, MemoryDataProviderName} BoltDataProviderName, MemoryDataProviderName}
// ValidPerms list that contains all the valid permissions for a user // ValidPerms defines all the valid permissions for a user
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete, ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes} PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes}
// ValidSSHLoginMethods list that contains all the valid SSH login methods // ValidSSHLoginMethods defines all the valid SSH login methods
ValidSSHLoginMethods = []string{SSHLoginMethodPublicKey, SSHLoginMethodPassword, SSHLoginMethodKeyboardInteractive} ValidSSHLoginMethods = []string{SSHLoginMethodPublicKey, SSHLoginMethodPassword, SSHLoginMethodKeyboardInteractive,
config Config SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
provider Provider // SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
sqlPlaceholders []string SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, config Config
provider Provider
sqlPlaceholders []string
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
pbkdf2SHA512Prefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix} pbkdf2SHA512Prefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix} pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix} unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
@ -311,7 +314,7 @@ func GetQuotaTracking() int {
// Provider interface that data providers must implement. // Provider interface that data providers must implement.
type Provider interface { type Provider interface {
validateUserAndPass(username string, password string) (User, error) validateUserAndPass(username string, password string) (User, error)
validateUserAndPubKey(username string, pubKey string) (User, string, error) validateUserAndPubKey(username string, pubKey []byte) (User, string, error)
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
getUsedQuota(username string) (int, int64, error) getUsedQuota(username string) (int, int64, error)
userExists(username string) (User, error) userExists(username string) (User, error)
@ -401,7 +404,7 @@ func InitializeDatabase(cnf Config, basePath string) error {
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error // CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
func CheckUserAndPass(p Provider, username string, password string) (User, error) { func CheckUserAndPass(p Provider, username string, password string) (User, error) {
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) { if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err := doExternalAuth(username, password, "", "") user, err := doExternalAuth(username, password, nil, "")
if err != nil { if err != nil {
return user, err return user, err
} }
@ -418,7 +421,7 @@ func CheckUserAndPass(p Provider, username string, password string) (User, error
} }
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error // CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
func CheckUserAndPubKey(p Provider, username string, pubKey string) (User, string, error) { func CheckUserAndPubKey(p Provider, username string, pubKey []byte) (User, string, error) {
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) { if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
user, err := doExternalAuth(username, "", pubKey, "") user, err := doExternalAuth(username, "", pubKey, "")
if err != nil { if err != nil {
@ -442,7 +445,7 @@ func CheckKeyboardInteractiveAuth(p Provider, username, authHook string, client
var user User var user User
var err error var err error
if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) { if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
user, err = doExternalAuth(username, "", "", "1") user, err = doExternalAuth(username, "", nil, "1")
} else if len(config.PreLoginHook) > 0 { } else if len(config.PreLoginHook) > 0 {
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive) user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive)
} else { } else {
@ -934,7 +937,7 @@ func checkUserAndPass(user User, password string) (User, error) {
return user, err return user, err
} }
func checkUserAndPubKey(user User, pubKey string) (User, string, error) { func checkUserAndPubKey(user User, pubKey []byte) (User, string, error) {
err := checkLoginConditions(user) err := checkLoginConditions(user)
if err != nil { if err != nil {
return user, "", err return user, "", err
@ -948,7 +951,7 @@ func checkUserAndPubKey(user User, pubKey string) (User, string, error) {
providerLog(logger.LevelWarn, "error parsing stored public key %d for user %v: %v", i, user.Username, err) providerLog(logger.LevelWarn, "error parsing stored public key %d for user %v: %v", i, user.Username, err)
return user, "", err return user, "", err
} }
if string(storedPubKey.Marshal()) == pubKey { if bytes.Equal(storedPubKey.Marshal(), pubKey) {
fp := ssh.FingerprintSHA256(storedPubKey) fp := ssh.FingerprintSHA256(storedPubKey)
return user, fp + ":" + comment, nil return user, fp + ":" + comment, nil
} }
@ -1451,7 +1454,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive strin
return cmd.Output() return cmd.Output()
} }
func doExternalAuth(username, password, pubKey, keyboardInteractive string) (User, error) { func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive string) (User, error) {
var user User var user User
pkey := "" pkey := ""
if len(pubKey) > 0 { if len(pubKey) > 0 {

View file

@ -91,7 +91,7 @@ func (p MemoryProvider) validateUserAndPass(username string, password string) (U
return checkUserAndPass(user, password) return checkUserAndPass(user, password)
} }
func (p MemoryProvider) validateUserAndPubKey(username string, pubKey string) (User, string, error) { func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
var user User var user User
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") return user, "", errors.New("Credentials cannot be null or empty")

View file

@ -66,7 +66,7 @@ func (p MySQLProvider) validateUserAndPass(username string, password string) (Us
return sqlCommonValidateUserAndPass(username, password, p.dbHandle) return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
} }
func (p MySQLProvider) validateUserAndPubKey(username string, publicKey string) (User, string, error) { func (p MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }

View file

@ -64,7 +64,7 @@ func (p PGSQLProvider) validateUserAndPass(username string, password string) (Us
return sqlCommonValidateUserAndPass(username, password, p.dbHandle) return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
} }
func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey string) (User, string, error) { func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }

View file

@ -44,7 +44,7 @@ func sqlCommonValidateUserAndPass(username string, password string, dbHandle *sq
return checkUserAndPass(user, password) return checkUserAndPass(user, password)
} }
func sqlCommonValidateUserAndPubKey(username string, pubKey string, dbHandle *sql.DB) (User, string, error) { func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sql.DB) (User, string, error) {
var user User var user User
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") return user, "", errors.New("Credentials cannot be null or empty")

View file

@ -63,7 +63,7 @@ func (p SQLiteProvider) validateUserAndPass(username string, password string) (U
return sqlCommonValidateUserAndPass(username, password, p.dbHandle) return sqlCommonValidateUserAndPass(username, password, p.dbHandle)
} }
func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey string) (User, string, error) { func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }

View file

@ -50,6 +50,8 @@ const (
SSHLoginMethodPublicKey = "publickey" SSHLoginMethodPublicKey = "publickey"
SSHLoginMethodPassword = "password" SSHLoginMethodPassword = "password"
SSHLoginMethodKeyboardInteractive = "keyboard-interactive" SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
SSHLoginMethodKeyAndPassword = "publickey+password"
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
) )
// ExtensionsFilter defines filters based on file extensions. // ExtensionsFilter defines filters based on file extensions.
@ -246,17 +248,73 @@ func (u *User) HasPerms(permissions []string, path string) bool {
return true return true
} }
// IsLoginMethodAllowed returns true if the specified login method is allowed for the user // IsLoginMethodAllowed returns true if the specified login method is allowed
func (u *User) IsLoginMethodAllowed(loginMetod string) bool { func (u *User) IsLoginMethodAllowed(loginMethod string, partialSuccessMethods []string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 { if len(u.Filters.DeniedLoginMethods) == 0 {
return true return true
} }
if utils.IsStringInSlice(loginMetod, u.Filters.DeniedLoginMethods) { if len(partialSuccessMethods) == 1 {
for _, method := range u.GetNextAuthMethods(partialSuccessMethods) {
if method == loginMethod {
return true
}
}
}
if utils.IsStringInSlice(loginMethod, u.Filters.DeniedLoginMethods) {
return false return false
} }
return true return true
} }
// GetNextAuthMethods returns the list of authentications methods that
// can continue for multi-step authentication
func (u *User) GetNextAuthMethods(partialSuccessMethods []string) []string {
var methods []string
if len(partialSuccessMethods) != 1 {
return methods
}
if partialSuccessMethods[0] != SSHLoginMethodPublicKey {
return methods
}
for _, method := range u.GetAllowedLoginMethods() {
if method == SSHLoginMethodKeyAndPassword {
methods = append(methods, SSHLoginMethodPassword)
}
if method == SSHLoginMethodKeyAndKeyboardInt {
methods = append(methods, SSHLoginMethodKeyboardInteractive)
}
}
return methods
}
// IsPartialAuth returns true if the specified login method is a step for
// a multi-step Authentication.
// We support publickey+password and publickey+keyboard-interactive, so
// only publickey can returns partial success.
// We can have partial success if only multi-step Auth methods are enabled
func (u *User) IsPartialAuth(loginMethod string) bool {
if loginMethod != SSHLoginMethodPublicKey {
return false
}
for _, method := range u.GetAllowedLoginMethods() {
if !utils.IsStringInSlice(method, SSHMultiStepsLoginMethods) {
return false
}
}
return true
}
// GetAllowedLoginMethods returns the allowed login methods
func (u *User) GetAllowedLoginMethods() []string {
var allowedMethods []string
for _, method := range ValidSSHLoginMethods {
if !utils.IsStringInSlice(method, u.Filters.DeniedLoginMethods) {
allowedMethods = append(allowedMethods, method)
}
}
return allowedMethods
}
// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters // IsFileAllowed returns true if the specified file is allowed by the file restrictions filters
func (u *User) IsFileAllowed(sftpPath string) bool { func (u *User) IsFileAllowed(sftpPath string) bool {
if len(u.Filters.FileExtensions) == 0 { if len(u.Filters.FileExtensions) == 0 {

View file

@ -30,10 +30,12 @@ For each account, the following properties can be configured:
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited. - `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited.
- `allowed_ip`, List of IP/Mask allowed to login. Any IP address not contained in this list cannot login. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32" - `allowed_ip`, List of IP/Mask allowed to login. Any IP address not contained in this list cannot login. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
- `denied_ip`, List of IP/Mask not allowed to login. If an IP address is both allowed and denied then login will be denied - `denied_ip`, List of IP/Mask not allowed to login. If an IP address is both allowed and denied then login will be denied
- `denied_login_methods`, List of login methods not allowed. The following login methods are supported: - `denied_login_methods`, List of login methods not allowed. To enable multi-step authentication you have to allow only multi-step login methods. The following login methods are supported:
- `publickey` - `publickey`
- `password` - `password`
- `keyboard-interactive` - `keyboard-interactive`
- `publickey+password`
- `publickey+keyboard-interactive`
- `file_extensions`, list of struct. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be listed in the list of files. Please note that these restrictions can be easily bypassed. Each struct contains the following fields: - `file_extensions`, list of struct. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be listed in the list of files. Please note that these restrictions can be easily bypassed. Each struct contains the following fields:
- `allowed_extensions`, list of, case insensitive, allowed files extension. Shell like expansion is not supported so you have to specify `.jpg` and not `*.jpg`. Any file that does not end with this suffix will be denied - `allowed_extensions`, list of, case insensitive, allowed files extension. Shell like expansion is not supported so you have to specify `.jpg` and not `*.jpg`. Any file that does not end with this suffix will be denied
- `denied_extensions`, list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones - `denied_extensions`, list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones

View file

@ -10,7 +10,7 @@ Several counters and gauges are available, for example:
- Total SSH command errors - Total SSH command errors
- Number of active connections - Number of active connections
- Data provider availability - Data provider availability
- Total successful and failed logins using password, public key or keyboard interactive authentication - Total successful and failed logins using password, public key, keyboard interactive authentication or supported multi-step authentications
- Total HTTP requests served and totals for response code - Total HTTP requests served and totals for response code
- Go's runtime details about GC, number of gouroutines and OS threads - Go's runtime details about GC, number of gouroutines and OS threads
- Process information like CPU, memory, file descriptor usage and start time - Process information like CPU, memory, file descriptor usage and start time

4
go.mod
View file

@ -43,6 +43,6 @@ require (
) )
replace ( replace (
github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20200315002837-010186aaa07d github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20200327213700-f3a27a751cdc
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20200403175740-a4c92a934d79 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20200409210311-95730af1ff98
) )

8
go.sum
View file

@ -61,10 +61,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/drakkan/crypto v0.0.0-20200403175740-a4c92a934d79 h1:dARy4jBz2xeVoGq3CTZ8dsQTXB4G833M5cIq7rMtEuw= github.com/drakkan/crypto v0.0.0-20200409210311-95730af1ff98 h1:5yTRWoqE1GhPfctw9E0wh1d3+xHajMyhQAQuxuNqwQ8=
github.com/drakkan/crypto v0.0.0-20200403175740-a4c92a934d79/go.mod h1:v3bhWOXGYda7H5d2s5t9XA6th3fxW3s0MQxU1R96G/w= github.com/drakkan/crypto v0.0.0-20200409210311-95730af1ff98/go.mod h1:v3bhWOXGYda7H5d2s5t9XA6th3fxW3s0MQxU1R96G/w=
github.com/drakkan/pipeat v0.0.0-20200315002837-010186aaa07d h1:qD1b7ZnrTUscSof+W+Pa3D9hN4jmQ/UcoZ05q7W96rA= github.com/drakkan/pipeat v0.0.0-20200327213700-f3a27a751cdc h1:aDQn2DUfujideR+efH3697MZk+9YY4T95P7BujKkWiY=
github.com/drakkan/pipeat v0.0.0-20200315002837-010186aaa07d/go.mod h1:wNYvIpR5rIhoezOYcpxcXz4HbIEOu7A45EqlQCA+h+w= github.com/drakkan/pipeat v0.0.0-20200327213700-f3a27a751cdc/go.mod h1:+JPhBw5JdJrSF80r6xsSg1TYHjyAGxYs4X24VyUdMZU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=

View file

@ -327,8 +327,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("unexpected error adding user with invalid filters: %v", err) t.Errorf("unexpected error adding user with invalid filters: %v", err)
} }
u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, u.Filters.DeniedLoginMethods = dataprovider.ValidSSHLoginMethods
dataprovider.SSHLoginMethodPassword, dataprovider.SSHLoginMethodPublicKey}
_, _, err = httpd.AddUser(u, http.StatusBadRequest) _, _, err = httpd.AddUser(u, http.StatusBadRequest)
if err != nil { if err != nil {
t.Errorf("unexpected error adding user with invalid filters: %v", err) t.Errorf("unexpected error adding user with invalid filters: %v", err)

View file

@ -933,6 +933,8 @@ components:
- 'publickey' - 'publickey'
- 'password' - 'password'
- 'keyboard-interactive' - 'keyboard-interactive'
- 'publickey+password'
- 'publickey+keyboard-interactive'
ExtensionsFilter: ExtensionsFilter:
type: object type: object
properties: properties:

View file

@ -148,6 +148,48 @@ var (
Help: "The total number of failed logins using keyboard interactive authentication", Help: "The total number of failed logins using keyboard interactive authentication",
}) })
// totalKeyAndPasswordLoginAttempts is the metric that reports the total number of
// login attempts using public key + password multi steps auth
totalKeyAndPasswordLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_password_login_attempts_total",
Help: "The total number of login attempts using public key + password",
})
// totalKeyAndPasswordLoginOK is the metric that reports the total number of
// successful logins using public key + password multi steps auth
totalKeyAndPasswordLoginOK = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_password_login_ok_total",
Help: "The total number of successful logins using public key + password",
})
// totalKeyAndPasswordLoginFailed is the metric that reports the total number of
// failed logins using public key + password multi steps auth
totalKeyAndPasswordLoginFailed = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_password_login_ko_total",
Help: "The total number of failed logins using public key + password",
})
// totalKeyAndKeyIntLoginAttempts is the metric that reports the total number of
// login attempts using public key + keyboard interactive multi steps auth
totalKeyAndKeyIntLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_keyboard_int_login_attempts_total",
Help: "The total number of login attempts using public key + keyboard interactive",
})
// totalKeyAndKeyIntLoginOK is the metric that reports the total number of
// successful logins using public key + keyboard interactive multi steps auth
totalKeyAndKeyIntLoginOK = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_keyboard_int_login_ok_total",
Help: "The total number of successful logins using public key + keyboard interactive",
})
// totalKeyAndKeyIntLoginFailed is the metric that reports the total number of
// failed logins using public key + keyboard interactive multi steps auth
totalKeyAndKeyIntLoginFailed = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_key_and_keyboard_int_login_ko_total",
Help: "The total number of failed logins using public key + keyboard interactive",
})
totalHTTPRequests = promauto.NewCounter(prometheus.CounterOpts{ totalHTTPRequests = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_http_req_total", Name: "sftpgo_http_req_total",
Help: "The total number of HTTP requests served", Help: "The total number of HTTP requests served",
@ -498,6 +540,10 @@ func AddLoginAttempt(authMethod string) {
totalKeyLoginAttempts.Inc() totalKeyLoginAttempts.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginAttempts.Inc() totalInteractiveLoginAttempts.Inc()
case "publickey+password":
totalKeyAndPasswordLoginAttempts.Inc()
case "publickey+keyboard-interactive":
totalKeyAndKeyIntLoginAttempts.Inc()
default: default:
totalPasswordLoginAttempts.Inc() totalPasswordLoginAttempts.Inc()
} }
@ -512,6 +558,10 @@ func AddLoginResult(authMethod string, err error) {
totalKeyLoginOK.Inc() totalKeyLoginOK.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginOK.Inc() totalInteractiveLoginOK.Inc()
case "publickey+password":
totalKeyAndPasswordLoginOK.Inc()
case "publickey+keyboard-interactive":
totalKeyAndKeyIntLoginOK.Inc()
default: default:
totalPasswordLoginOK.Inc() totalPasswordLoginOK.Inc()
} }
@ -522,6 +572,10 @@ func AddLoginResult(authMethod string, err error) {
totalKeyLoginFailed.Inc() totalKeyLoginFailed.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginFailed.Inc() totalInteractiveLoginFailed.Inc()
case "publickey+password":
totalKeyAndPasswordLoginFailed.Inc()
case "publickey+keyboard-interactive":
totalKeyAndKeyIntLoginFailed.Inc()
default: default:
totalPasswordLoginFailed.Inc() totalPasswordLoginFailed.Inc()
} }

View file

@ -503,7 +503,8 @@ def addCommonUserArguments(parser):
'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Permissions for the root directory ' 'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Permissions for the root directory '
+'(/). Default: %(default)s') +'(/). Default: %(default)s')
parser.add_argument('-L', '--denied-login-methods', type=str, nargs='+', default=[], parser.add_argument('-L', '--denied-login-methods', type=str, nargs='+', default=[],
choices=['', 'publickey', 'password', 'keyboard-interactive'], help='Default: %(default)s') choices=['', 'publickey', 'password', 'keyboard-interactive', 'publickey+password',
'publickey+keyboard-interactive'], help='Default: %(default)s')
parser.add_argument('--subdirs-permissions', type=str, nargs='*', default=[], help='Permissions for subdirs. ' parser.add_argument('--subdirs-permissions', type=str, nargs='*', default=[], help='Permissions for subdirs. '
+'For example: "/somedir::list,download" "/otherdir/subdir::*" Default: %(default)s') +'For example: "/somedir::list,download" "/otherdir/subdir::*" Default: %(default)s')
parser.add_argument('--virtual-folders', type=str, nargs='*', default=[], help='Virtual folder mapping. For example: ' parser.add_argument('--virtual-folders', type=str, nargs='*', default=[], help='Virtual folder mapping. For example: '

View file

@ -484,7 +484,7 @@ func TestUploadFiles(t *testing.T) {
func TestWithInvalidHome(t *testing.T) { func TestWithInvalidHome(t *testing.T) {
u := dataprovider.User{} u := dataprovider.User{}
u.HomeDir = "home_rel_path" u.HomeDir = "home_rel_path"
_, err := loginUser(u, dataprovider.SSHLoginMethodPassword, "", "") _, err := loginUser(u, dataprovider.SSHLoginMethodPassword, "", nil)
if err == nil { if err == nil {
t.Errorf("login a user with an invalid home_dir must fail") t.Errorf("login a user with an invalid home_dir must fail")
} }

View file

@ -158,42 +158,32 @@ func (c Configuration) Initialize(configDir string) error {
return sp, nil return sp, nil
}, },
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
sp, err := c.validatePublicKeyCredentials(conn, string(pubKey.Marshal())) sp, err := c.validatePublicKeyCredentials(conn, pubKey.Marshal())
if err == ssh.ErrPartialSuccess {
return nil, err
}
if err != nil { if err != nil {
return nil, &authenticationError{err: fmt.Sprintf("could not validate public key credentials: %v", err)} return nil, &authenticationError{err: fmt.Sprintf("could not validate public key credentials: %v", err)}
} }
return sp, nil return sp, nil
}, },
ServerVersion: "SSH-2.0-" + c.Banner, NextAuthMethodsCallback: func(conn ssh.ConnMetadata) []string {
var nextMethods []string
user, err := dataprovider.UserExists(dataProvider, conn.User())
if err == nil {
nextMethods = user.GetNextAuthMethods(conn.PartialSuccessMethods())
}
return nextMethods
},
ServerVersion: fmt.Sprintf("SSH-2.0-%v", c.Banner),
} }
err = c.checkHostKeys(configDir) err = c.checkAndLoadHostKeys(configDir, serverConfig)
if err != nil { if err != nil {
return err return err
} }
for _, k := range c.Keys {
privateFile := k.PrivateKey
if !filepath.IsAbs(privateFile) {
privateFile = filepath.Join(configDir, privateFile)
}
logger.Info(logSender, "", "Loading private key: %s", privateFile)
privateBytes, err := ioutil.ReadFile(privateFile)
if err != nil {
return err
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return err
}
// Add private key to the server configuration.
serverConfig.AddHostKey(private)
}
c.configureSecurityOptions(serverConfig) c.configureSecurityOptions(serverConfig)
c.configureKeyboardInteractiveAuth(serverConfig) c.configureKeyboardInteractiveAuth(serverConfig)
c.configureLoginBanner(serverConfig, configDir) c.configureLoginBanner(serverConfig, configDir)
@ -285,9 +275,10 @@ func (c Configuration) configureLoginBanner(serverConfig *ssh.ServerConfig, conf
if !filepath.IsAbs(bannerFilePath) { if !filepath.IsAbs(bannerFilePath) {
bannerFilePath = filepath.Join(configDir, bannerFilePath) bannerFilePath = filepath.Join(configDir, bannerFilePath)
} }
var banner []byte var bannerContent []byte
banner, err = ioutil.ReadFile(bannerFilePath) bannerContent, err = ioutil.ReadFile(bannerFilePath)
if err == nil { if err == nil {
banner := string(bannerContent)
serverConfig.BannerCallback = func(conn ssh.ConnMetadata) string { serverConfig.BannerCallback = func(conn ssh.ConnMetadata) string {
return string(banner) return string(banner)
} }
@ -459,9 +450,13 @@ func (c Configuration) createHandler(connection Connection) sftp.Handlers {
} }
} }
func loginUser(user dataprovider.User, loginMethod, remoteAddr, publicKey string) (*ssh.Permissions, error) { func loginUser(user dataprovider.User, loginMethod, publicKey string, conn ssh.ConnMetadata) (*ssh.Permissions, error) {
connectionID := ""
if conn != nil {
connectionID = hex.EncodeToString(conn.SessionID())
}
if !filepath.IsAbs(user.HomeDir) { if !filepath.IsAbs(user.HomeDir) {
logger.Warn(logSender, "", "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed", logger.Warn(logSender, connectionID, "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
user.Username, user.HomeDir) user.Username, user.HomeDir)
return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir) return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
} }
@ -473,18 +468,19 @@ func loginUser(user dataprovider.User, loginMethod, remoteAddr, publicKey string
return nil, fmt.Errorf("too many open sessions: %v", activeSessions) return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
} }
} }
if !user.IsLoginMethodAllowed(loginMethod) { if !user.IsLoginMethodAllowed(loginMethod, conn.PartialSuccessMethods()) {
logger.Debug(logSender, "", "cannot login user %#v, login method %#v is not allowed", user.Username, loginMethod) logger.Debug(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) return nil, fmt.Errorf("Login method %#v is not allowed for user %#v", loginMethod, user.Username)
} }
remoteAddr := conn.RemoteAddr().String()
if !user.IsLoginFromAddrAllowed(remoteAddr) { if !user.IsLoginFromAddrAllowed(remoteAddr) {
logger.Debug(logSender, "", "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr) logger.Debug(logSender, connectionID, "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr)
return nil, fmt.Errorf("Login for user %#v is not allowed from this address: %v", user.Username, remoteAddr) return nil, fmt.Errorf("Login for user %#v is not allowed from this address: %v", user.Username, remoteAddr)
} }
json, err := json.Marshal(user) json, err := json.Marshal(user)
if err != nil { if err != nil {
logger.Warn(logSender, "", "error serializing user info: %v, authentication rejected", err) logger.Warn(logSender, connectionID, "error serializing user info: %v, authentication rejected", err)
return nil, err return nil, err
} }
if len(publicKey) > 0 { if len(publicKey) > 0 {
@ -514,8 +510,8 @@ func (c *Configuration) checkSSHCommands() {
c.EnabledSSHCommands = sshCommands c.EnabledSSHCommands = sshCommands
} }
// If no host keys are defined we try to use or generate the default one. // If no host keys are defined we try to use or generate the default ones.
func (c *Configuration) checkHostKeys(configDir string) error { func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {
if len(c.Keys) == 0 { if len(c.Keys) == 0 {
defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName} defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName}
for _, k := range defaultKeys { for _, k := range defaultKeys {
@ -535,20 +531,45 @@ func (c *Configuration) checkHostKeys(configDir string) error {
c.Keys = append(c.Keys, Key{PrivateKey: k}) c.Keys = append(c.Keys, Key{PrivateKey: k})
} }
} }
for _, k := range c.Keys {
privateFile := k.PrivateKey
if !filepath.IsAbs(privateFile) {
privateFile = filepath.Join(configDir, privateFile)
}
logger.Info(logSender, "", "Loading private key: %s", privateFile)
privateBytes, err := ioutil.ReadFile(privateFile)
if err != nil {
return err
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return err
}
// Add private key to the server configuration.
serverConfig.AddHostKey(private)
}
return nil return nil
} }
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey string) (*ssh.Permissions, error) { func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey []byte) (*ssh.Permissions, error) {
var err error var err error
var user dataprovider.User var user dataprovider.User
var keyID string var keyID string
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
connectionID := hex.EncodeToString(conn.SessionID())
method := dataprovider.SSHLoginMethodPublicKey method := dataprovider.SSHLoginMethodPublicKey
metrics.AddLoginAttempt(method)
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey); err == nil { if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey); err == nil {
sshPerm, err = loginUser(user, method, conn.RemoteAddr().String(), keyID) if user.IsPartialAuth(method) {
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
return nil, ssh.ErrPartialSuccess
}
sshPerm, err = loginUser(user, method, keyID, conn)
} }
metrics.AddLoginAttempt(method)
if err != nil { if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error()) logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
} }
@ -562,9 +583,12 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
method := dataprovider.SSHLoginMethodPassword method := dataprovider.SSHLoginMethodPassword
if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndPassword
}
metrics.AddLoginAttempt(method) metrics.AddLoginAttempt(method)
if user, err = dataprovider.CheckUserAndPass(dataProvider, conn.User(), string(pass)); err == nil { if user, err = dataprovider.CheckUserAndPass(dataProvider, conn.User(), string(pass)); err == nil {
sshPerm, err = loginUser(user, method, conn.RemoteAddr().String(), "") sshPerm, err = loginUser(user, method, "", conn)
} }
if err != nil { if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error()) logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
@ -579,9 +603,12 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
method := dataprovider.SSHLoginMethodKeyboardInteractive method := dataprovider.SSHLoginMethodKeyboardInteractive
if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
}
metrics.AddLoginAttempt(method) metrics.AddLoginAttempt(method)
if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveHook, client); err == nil { if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveHook, client); err == nil {
sshPerm, err = loginUser(user, method, conn.RemoteAddr().String(), "") sshPerm, err = loginUser(user, method, "", conn)
} }
if err != nil { if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error()) logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())

View file

@ -999,6 +999,120 @@ func TestLogin(t *testing.T) {
os.RemoveAll(user.GetHomeDir()) os.RemoveAll(user.GetHomeDir())
} }
func TestMultiStepLoginKeyAndPwd(t *testing.T) {
u := getTestUser(true)
u.Password = defaultPassword
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive,
}...)
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
_, err = getSftpClient(user, true)
if err == nil {
t.Error("login with public key is disallowed and must fail")
}
_, err = getSftpClient(user, true)
if err == nil {
t.Error("login with password is disallowed and must fail")
}
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
authMethods := []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.Password(defaultPassword),
}
client, err := getCustomAuthSftpClient(user, authMethods)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err := client.Getwd()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
authMethods = []ssh.AuthMethod{
ssh.Password(defaultPassword),
ssh.PublicKeys(key),
}
_, err = getCustomAuthSftpClient(user, authMethods)
if err == nil {
t.Error("multi step auth login with wrong order must fail")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
os.RemoveAll(user.GetHomeDir())
}
func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this test is not available on Windows")
}
u := getTestUser(true)
u.Password = defaultPassword
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
dataprovider.SSHLoginMethodKeyAndPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive,
}...)
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), 0755)
_, err = getSftpClient(user, true)
if err == nil {
t.Error("login with public key is disallowed and must fail")
}
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
authMethods := []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"1", "2"}, nil
}),
}
client, err := getCustomAuthSftpClient(user, authMethods)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err := client.Getwd()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
authMethods = []ssh.AuthMethod{
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"1", "2"}, nil
}),
ssh.PublicKeys(key),
}
_, err = getCustomAuthSftpClient(user, authMethods)
if err == nil {
t.Error("multi step auth login with wrong order must fail")
}
authMethods = []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.Password(defaultPassword),
}
_, err = getCustomAuthSftpClient(user, authMethods)
if err == nil {
t.Error("multi step auth login with wrong method must fail")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
os.RemoveAll(user.GetHomeDir())
}
func TestLoginUserStatus(t *testing.T) { func TestLoginUserStatus(t *testing.T) {
usePubKey := true usePubKey := true
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
@ -3823,6 +3937,151 @@ func TestFilterFileExtensions(t *testing.T) {
} }
} }
func TestUserAllowedLoginMethods(t *testing.T) {
user := getTestUser(true)
user.Filters.DeniedLoginMethods = dataprovider.ValidSSHLoginMethods
allowedMethods := user.GetAllowedLoginMethods()
if len(allowedMethods) != 0 {
t.Errorf("unexpected allowed methods: %+v", allowedMethods)
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
allowedMethods = user.GetAllowedLoginMethods()
if len(allowedMethods) != 2 {
t.Errorf("unexpected allowed methods: %+v", allowedMethods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndKeyboardInt, allowedMethods) {
t.Errorf("unexpected allowed methods: %+v", allowedMethods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndPassword, allowedMethods) {
t.Errorf("unexpected allowed methods: %+v", allowedMethods)
}
}
func TestUserPartialAuth(t *testing.T) {
user := getTestUser(true)
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
if user.IsPartialAuth(dataprovider.SSHLoginMethodPassword) {
t.Error("unexpected partial auth method")
}
if user.IsPartialAuth(dataprovider.SSHLoginMethodKeyboardInteractive) {
t.Error("unexpected partial auth method")
}
if !user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey) {
t.Error("public key must be a partial auth method with this configuration")
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
if user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey) {
t.Error("public key must not be a partial auth method with this configuration")
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
}
if user.IsPartialAuth(dataprovider.SSHLoginMethodPublicKey) {
t.Error("public key must not be a partial auth method with this configuration")
}
}
func TestUserGetNextAuthMethods(t *testing.T) {
user := getTestUser(true)
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
methods := user.GetNextAuthMethods(nil)
if len(methods) != 0 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPassword})
if len(methods) != 0 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodKeyboardInteractive})
if len(methods) != 0 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
methods = user.GetNextAuthMethods([]string{
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
})
if len(methods) != 0 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
if len(methods) != 2 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodPassword, methods) {
t.Errorf("unexpected next auth methods: %+v", methods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods) {
t.Errorf("unexpected next auth methods: %+v", methods)
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
}
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
if len(methods) != 1 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodPassword, methods) {
t.Errorf("unexpected next auth methods: %+v", methods)
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
dataprovider.SSHLoginMethodKeyAndPassword,
}
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey})
if len(methods) != 1 {
t.Errorf("unexpected next auth methods: %+v", methods)
}
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods) {
t.Errorf("unexpected next auth methods: %+v", methods)
}
}
func TestUserIsLoginMethodAllowed(t *testing.T) {
user := getTestUser(true)
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
if user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, nil) {
t.Error("unexpected login method allowed")
}
if !user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, []string{dataprovider.SSHLoginMethodPublicKey}) {
t.Error("unexpected login method denied")
}
if !user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, []string{dataprovider.SSHLoginMethodPublicKey}) {
t.Error("unexpected login method denied")
}
user.Filters.DeniedLoginMethods = []string{
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodKeyboardInteractive,
}
if !user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPassword, nil) {
t.Error("unexpected login method denied")
}
}
func TestUserEmptySubDirPerms(t *testing.T) { func TestUserEmptySubDirPerms(t *testing.T) {
user := getTestUser(true) user := getTestUser(true)
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
@ -5041,6 +5300,23 @@ func getKeyboardInteractiveSftpClient(user dataprovider.User, answers []string)
return sftpClient, err return sftpClient, err
} }
func getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMethod) (*sftp.Client, error) {
var sftpClient *sftp.Client
config := &ssh.ClientConfig{
User: user.Username,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Auth: authMethods,
}
conn, err := ssh.Dial("tcp", sftpServerAddr, config)
if err != nil {
return sftpClient, err
}
sftpClient, err = sftp.NewClient(conn)
return sftpClient, err
}
func createTestFile(path string, size int64) error { func createTestFile(path string, size int64) error {
baseDir := filepath.Dir(path) baseDir := filepath.Dir(path)
if _, err := os.Stat(baseDir); os.IsNotExist(err) { if _, err := os.Stat(baseDir); os.IsNotExist(err) {