add support for per user authentication methods

You can, for example, deny one or more authentication methods to one or
more users.
This commit is contained in:
Nicola Murino
2020-02-19 22:39:30 +01:00
parent 62b20cd884
commit bc11cdd8d5
15 changed files with 264 additions and 87 deletions

View File

@@ -10,6 +10,7 @@ Full featured and highly configurable SFTP server
- 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.
- Per user authentication methods. You can, for example, deny one or more authentication methods to one or more users.
- Custom authentication using external programs is supported. - Custom authentication using external programs is supported.
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files. - Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
- Bandwidth throttling is supported, with distinct settings for upload and download. - Bandwidth throttling is supported, with distinct settings for upload and download.
@@ -687,6 +688,10 @@ 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:
- `publickey`
- `password`
- `keyboard-interactive`
- `fs_provider`, filesystem to serve via SFTP. Local filesystem and S3 Compatible Object Storage are supported - `fs_provider`, filesystem to serve via SFTP. Local filesystem and S3 Compatible Object Storage are supported
- `s3_bucket`, required for S3 filesystem - `s3_bucket`, required for S3 filesystem
- `s3_region`, required for S3 filesystem. Must match the region for your bucket. You can find here the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example if your bucket is at `Frankfurt` you have to set the region to `eu-central-1` - `s3_region`, required for S3 filesystem. Must match the region for your bucket. You can find here the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example if your bucket is at `Frankfurt` you have to set the region to `eu-central-1`
@@ -825,7 +830,7 @@ The logs can be divided into the following categories:
- `level` string - `level` string
- `username`, string. Can be empty if the connection is closed before an authentication attempt - `username`, string. Can be empty if the connection is closed before an authentication attempt
- `client_ip` string. - `client_ip` string.
- `login_type` string. Can be `public_key`, `password` or `no_auth_tryed` - `login_type` string. Can be `publickey`, `password`, `keyboard-interactive` or `no_auth_tryed`
- `error` string. Optional error description - `error` string. Optional error description
### Brute force protection ### Brute force protection

View File

@@ -77,10 +77,12 @@ var (
// ValidPerms list that contains all the valid permissions for an user // ValidPerms list that contains all the valid permissions for an 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}
config Config // ValidSSHLoginMethods list that contains all the valid SSH login methods
provider Provider ValidSSHLoginMethods = []string{SSHLoginMethodPublicKey, SSHLoginMethodPassword, SSHLoginMethodKeyboardInteractive}
sqlPlaceholders []string config Config
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, 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}
@@ -510,6 +512,9 @@ func validatePermissions(user *User) error {
if len(perms) == 0 && dir == "/" { if len(perms) == 0 && dir == "/" {
return &ValidationError{err: fmt.Sprintf("no permissions granted for the directory: %#v", dir)} return &ValidationError{err: fmt.Sprintf("no permissions granted for the directory: %#v", dir)}
} }
if len(perms) > len(ValidPerms) {
return &ValidationError{err: "invalid permissions"}
}
for _, p := range perms { for _, p := range perms {
if !utils.IsStringInSlice(p, ValidPerms) { if !utils.IsStringInSlice(p, ValidPerms) {
return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", p)} return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", p)}
@@ -555,6 +560,9 @@ func validateFilters(user *User) error {
if len(user.Filters.DeniedIP) == 0 { if len(user.Filters.DeniedIP) == 0 {
user.Filters.DeniedIP = []string{} user.Filters.DeniedIP = []string{}
} }
if len(user.Filters.DeniedLoginMethods) == 0 {
user.Filters.DeniedLoginMethods = []string{}
}
for _, IPMask := range user.Filters.DeniedIP { for _, IPMask := range user.Filters.DeniedIP {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
@@ -567,6 +575,14 @@ func validateFilters(user *User) error {
return &ValidationError{err: fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)} return &ValidationError{err: fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)}
} }
} }
if len(user.Filters.DeniedLoginMethods) >= len(ValidSSHLoginMethods) {
return &ValidationError{err: "invalid denied_login_methods"}
}
for _, loginMethod := range user.Filters.DeniedLoginMethods {
if !utils.IsStringInSlice(loginMethod, ValidSSHLoginMethods) {
return &ValidationError{err: fmt.Sprintf("invalid login method: %#v", loginMethod)}
}
}
return nil return nil
} }

View File

@@ -42,15 +42,25 @@ const (
PermChtimes = "chtimes" PermChtimes = "chtimes"
) )
// Available SSH login methods
const (
SSHLoginMethodPublicKey = "publickey"
SSHLoginMethodPassword = "password"
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
)
// UserFilters defines additional restrictions for a user // UserFilters defines additional restrictions for a user
type UserFilters struct { type UserFilters struct {
// only clients connecting from these IP/Mask are allowed. // only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291 // 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" // for example "192.0.2.0/24" or "2001:db8::/32"
AllowedIP []string `json:"allowed_ip"` AllowedIP []string `json:"allowed_ip,omitempty"`
// clients connecting from these IP/Mask are not allowed. // clients connecting from these IP/Mask are not allowed.
// Denied rules will be evaluated before allowed ones // Denied rules will be evaluated before allowed ones
DeniedIP []string `json:"denied_ip"` DeniedIP []string `json:"denied_ip,omitempty"`
// these login methods are not allowed.
// If null or empty any available login method is allowed
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
} }
// Filesystem defines cloud storage filesystem details // Filesystem defines cloud storage filesystem details
@@ -183,11 +193,22 @@ func (u *User) HasPerms(permissions []string, path string) bool {
return true return true
} }
// IsLoginAllowed return true if the login is allowed from the specified remoteAddr. // IsLoginMethodAllowed returns true if the specified login method is allowed for the user
func (u *User) IsLoginMethodAllowed(loginMetod string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 {
return true
}
if utils.IsStringInSlice(loginMetod, u.Filters.DeniedLoginMethods) {
return false
}
return true
}
// IsLoginFromAddrAllowed returns true if the login is allowed from the specified remoteAddr.
// If AllowedIP is defined only the specified IP/Mask can login. // If AllowedIP is defined only the specified IP/Mask can login.
// If DeniedIP is defined the specified IP/Mask cannot login. // If DeniedIP is defined the specified IP/Mask cannot login.
// If an IP is both allowed and denied then login will be denied // If an IP is both allowed and denied then login will be denied
func (u *User) IsLoginAllowed(remoteAddr string) bool { func (u *User) IsLoginFromAddrAllowed(remoteAddr string) bool {
if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 { if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 {
return true return true
} }
@@ -407,6 +428,8 @@ func (u *User) getACopy() User {
copy(filters.AllowedIP, u.Filters.AllowedIP) copy(filters.AllowedIP, u.Filters.AllowedIP)
filters.DeniedIP = make([]string, len(u.Filters.DeniedIP)) filters.DeniedIP = make([]string, len(u.Filters.DeniedIP))
copy(filters.DeniedIP, u.Filters.DeniedIP) copy(filters.DeniedIP, u.Filters.DeniedIP)
filters.DeniedLoginMethods = make([]string, len(u.Filters.DeniedLoginMethods))
copy(filters.DeniedLoginMethods, u.Filters.DeniedLoginMethods)
fsConfig := Filesystem{ fsConfig := Filesystem{
Provider: u.FsConfig.Provider, Provider: u.FsConfig.Provider,
S3Config: vfs.S3FsConfig{ S3Config: vfs.S3FsConfig{

View File

@@ -503,6 +503,9 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
if len(expected.Filters.DeniedIP) != len(actual.Filters.DeniedIP) { if len(expected.Filters.DeniedIP) != len(actual.Filters.DeniedIP) {
return errors.New("DeniedIP mismatch") return errors.New("DeniedIP mismatch")
} }
if len(expected.Filters.DeniedLoginMethods) != len(actual.Filters.DeniedLoginMethods) {
return errors.New("Denied login methods mismatch")
}
for _, IPMask := range expected.Filters.AllowedIP { for _, IPMask := range expected.Filters.AllowedIP {
if !utils.IsStringInSlice(IPMask, actual.Filters.AllowedIP) { if !utils.IsStringInSlice(IPMask, actual.Filters.AllowedIP) {
return errors.New("AllowedIP contents mismatch") return errors.New("AllowedIP contents mismatch")
@@ -513,6 +516,11 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
return errors.New("DeniedIP contents mismatch") return errors.New("DeniedIP contents mismatch")
} }
} }
for _, method := range expected.Filters.DeniedLoginMethods {
if !utils.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) {
return errors.New("Denied login methods contents mismatch")
}
}
return nil return nil
} }

View File

@@ -312,6 +312,18 @@ 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.DeniedIP = []string{}
u.Filters.DeniedLoginMethods = []string{"invalid"}
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
t.Errorf("unexpected error adding user with invalid filters: %v", err)
}
u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive,
dataprovider.SSHLoginMethodPassword, dataprovider.SSHLoginMethodPublicKey}
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
t.Errorf("unexpected error adding user with invalid filters: %v", err)
}
} }
func TestAddUserInvalidFsConfig(t *testing.T) { func TestAddUserInvalidFsConfig(t *testing.T) {
@@ -409,6 +421,7 @@ func TestUpdateUser(t *testing.T) {
user.Permissions["/subdir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload} user.Permissions["/subdir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}
user.Filters.AllowedIP = []string{"192.168.1.0/24", "192.168.2.0/24"} user.Filters.AllowedIP = []string{"192.168.1.0/24", "192.168.2.0/24"}
user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"} user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"}
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPassword}
user.UploadBandwidth = 1024 user.UploadBandwidth = 1024
user.DownloadBandwidth = 512 user.DownloadBandwidth = 512
user, _, err = httpd.UpdateUser(user, http.StatusOK) user, _, err = httpd.UpdateUser(user, http.StatusOK)
@@ -893,7 +906,7 @@ func TestDumpdata(t *testing.T) {
os.RemoveAll(credentialsPath) os.RemoveAll(credentialsPath)
err = dataprovider.Initialize(providerConf, configDir) err = dataprovider.Initialize(providerConf, configDir)
if err != nil { if err != nil {
t.Errorf("error initializing data provider") t.Errorf("error initializing data provider: %v", err)
} }
httpd.SetDataProvider(dataprovider.GetProvider()) httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider())
@@ -1720,6 +1733,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("expiration_date", "2020-01-01 00:00:00") form.Set("expiration_date", "2020-01-01 00:00:00")
form.Set("allowed_ip", " 192.168.1.3/32, 192.168.2.0/24 ") form.Set("allowed_ip", " 192.168.1.3/32, 192.168.2.0/24 ")
form.Set("denied_ip", " 10.0.0.2/32 ") form.Set("denied_ip", " 10.0.0.2/32 ")
form.Set("ssh_login_methods", dataprovider.SSHLoginMethodKeyboardInteractive)
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b) req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b)
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
@@ -1765,6 +1779,9 @@ func TestWebUserUpdateMock(t *testing.T) {
if !utils.IsStringInSlice("10.0.0.2/32", updateUser.Filters.DeniedIP) { if !utils.IsStringInSlice("10.0.0.2/32", updateUser.Filters.DeniedIP) {
t.Errorf("Denied IP/Mask does not match: %v", updateUser.Filters.DeniedIP) t.Errorf("Denied IP/Mask does not match: %v", updateUser.Filters.DeniedIP)
} }
if !utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, updateUser.Filters.DeniedLoginMethods) {
t.Errorf("Denied login methods does not match: %v", updateUser.Filters.DeniedLoginMethods)
}
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code) checkResponseCode(t, http.StatusOK, rr.Code)

View File

@@ -59,12 +59,6 @@ func TestCheckUser(t *testing.T) {
t.Errorf("actual password must be nil") t.Errorf("actual password must be nil")
} }
actual.Password = "" actual.Password = ""
actual.PublicKeys = []string{"pub key"}
err = checkUser(expected, actual)
if err == nil {
t.Errorf("actual public key must be nil")
}
actual.PublicKeys = []string{}
err = checkUser(expected, actual) err = checkUser(expected, actual)
if err == nil { if err == nil {
t.Errorf("actual ID must be > 0") t.Errorf("actual ID must be > 0")
@@ -104,9 +98,21 @@ func TestCheckUser(t *testing.T) {
} }
expected.Permissions = make(map[string][]string) expected.Permissions = make(map[string][]string)
actual.Permissions = make(map[string][]string) actual.Permissions = make(map[string][]string)
actual.FsConfig.Provider = 1
err = checkUser(expected, actual)
if err == nil {
t.Errorf("Fs providers are not equal")
}
}
func TestCompareUserFilters(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
actual.ID = 1
expected.ID = 1
expected.Filters.AllowedIP = []string{} expected.Filters.AllowedIP = []string{}
actual.Filters.AllowedIP = []string{"192.168.1.2/32"} actual.Filters.AllowedIP = []string{"192.168.1.2/32"}
err = checkUser(expected, actual) err := checkUser(expected, actual)
if err == nil { if err == nil {
t.Errorf("AllowedIP are not equal") t.Errorf("AllowedIP are not equal")
} }
@@ -130,10 +136,16 @@ func TestCheckUser(t *testing.T) {
} }
expected.Filters.DeniedIP = []string{} expected.Filters.DeniedIP = []string{}
actual.Filters.DeniedIP = []string{} actual.Filters.DeniedIP = []string{}
actual.FsConfig.Provider = 1 expected.Filters.DeniedLoginMethods = []string{}
actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey}
err = checkUser(expected, actual) err = checkUser(expected, actual)
if err == nil { if err == nil {
t.Errorf("Fs providers are not equal") t.Errorf("Denied login methods are not equal")
}
expected.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPassword}
err = checkUser(expected, actual)
if err == nil {
t.Errorf("Denied login methods contents are not equal")
} }
} }

View File

@@ -2,7 +2,7 @@ openapi: 3.0.1
info: info:
title: SFTPGo title: SFTPGo
description: 'SFTPGo REST API' description: 'SFTPGo REST API'
version: 1.8.0 version: 1.8.1
servers: servers:
- url: /api/v1 - url: /api/v1
@@ -927,6 +927,12 @@ components:
minItems: 1 minItems: 1
minProperties: 1 minProperties: 1
description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required
LoginMethods:
type: string
enum:
- 'publickey'
- 'password'
- 'keyboard-interactive'
UserFilters: UserFilters:
type: object type: object
properties: properties:
@@ -944,6 +950,12 @@ components:
nullable: true nullable: true
description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones
example: [ "172.16.0.0/16" ] example: [ "172.16.0.0/16" ]
denied_login_methods:
type: array
items:
$ref: '#/components/schemas/LoginMethods'
nullable: true
description: if null or empty any available login method is allowed
description: Additional restrictions description: Additional restrictions
S3Config: S3Config:
type: object type: object
@@ -973,8 +985,6 @@ components:
required: required:
- bucket - bucket
- region - region
- access_key
- access_secret
nullable: true nullable: true
description: S3 Compatible Object Storage configuration details description: S3 Compatible Object Storage configuration details
GCSConfig: GCSConfig:

View File

@@ -64,12 +64,13 @@ type connectionsPage struct {
type userPage struct { type userPage struct {
basePage basePage
IsAdd bool IsAdd bool
User dataprovider.User User dataprovider.User
RootPerms []string RootPerms []string
Error string Error string
ValidPerms []string ValidPerms []string
RootDirPerms []string ValidSSHLoginMethods []string
RootDirPerms []string
} }
type messagePage struct { type messagePage struct {
@@ -161,24 +162,26 @@ func renderNotFoundPage(w http.ResponseWriter, err error) {
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) { func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
data := userPage{ data := userPage{
basePage: getBasePageData("Add a new user", webUserPath), basePage: getBasePageData("Add a new user", webUserPath),
IsAdd: true, IsAdd: true,
Error: error, Error: error,
User: user, User: user,
ValidPerms: dataprovider.ValidPerms, ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"), ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
RootDirPerms: user.GetPermissionsForPath("/"),
} }
renderTemplate(w, templateUser, data) renderTemplate(w, templateUser, data)
} }
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) { func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
data := userPage{ data := userPage{
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)), basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
IsAdd: false, IsAdd: false,
Error: error, Error: error,
User: user, User: user,
ValidPerms: dataprovider.ValidPerms, ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"), ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
RootDirPerms: user.GetPermissionsForPath("/"),
} }
renderTemplate(w, templateUser, data) renderTemplate(w, templateUser, data)
} }
@@ -224,6 +227,7 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
var filters dataprovider.UserFilters var filters dataprovider.UserFilters
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",") filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
return filters return filters
} }

View File

@@ -494,7 +494,7 @@ func UpdateDataProviderAvailability(err error) {
func AddLoginAttempt(authMethod string) { func AddLoginAttempt(authMethod string) {
totalLoginAttempts.Inc() totalLoginAttempts.Inc()
switch authMethod { switch authMethod {
case "public_key": case "publickey":
totalKeyLoginAttempts.Inc() totalKeyLoginAttempts.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginAttempts.Inc() totalInteractiveLoginAttempts.Inc()
@@ -508,7 +508,7 @@ func AddLoginResult(authMethod string, err error) {
if err == nil { if err == nil {
totalLoginOK.Inc() totalLoginOK.Inc()
switch authMethod { switch authMethod {
case "public_key": case "publickey":
totalKeyLoginOK.Inc() totalKeyLoginOK.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginOK.Inc() totalInteractiveLoginOK.Inc()
@@ -518,7 +518,7 @@ func AddLoginResult(authMethod string, err error) {
} else { } else {
totalLoginFailed.Inc() totalLoginFailed.Inc()
switch authMethod { switch authMethod {
case "public_key": case "publickey":
totalKeyLoginFailed.Inc() totalKeyLoginFailed.Inc()
case "keyboard-interactive": case "keyboard-interactive":
totalInteractiveLoginFailed.Inc() totalInteractiveLoginFailed.Inc()

View File

@@ -44,7 +44,7 @@ Let's see a sample usage for each REST API.
Command: Command:
``` ```
python sftpgo_api_cli.py add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --subdirs-permissions "/dir1:list,download" "/dir2:*" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01 --allowed-ip "192.168.1.1/32" --fs S3 --s3-bucket test --s3-region eu-west-1 --s3-access-key accesskey --s3-access-secret secret --s3-endpoint "http://127.0.0.1:9000" --s3-storage-class Standard --s3-key-prefix "vfolder/" python sftpgo_api_cli.py add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --subdirs-permissions "/dir1:list,download" "/dir2:*" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01 --allowed-ip "192.168.1.1/32" --fs S3 --s3-bucket test --s3-region eu-west-1 --s3-access-key accesskey --s3-access-secret secret --s3-endpoint "http://127.0.0.1:9000" --s3-storage-class Standard --s3-key-prefix "vfolder/" --denied-login-methods "password" "keyboard-interactive"
``` ```
Output: Output:
@@ -70,7 +70,10 @@ Output:
"allowed_ip": [ "allowed_ip": [
"192.168.1.1/32" "192.168.1.1/32"
], ],
"denied_ip": [] "denied_login_methods": [
"password",
"keyboard-interactive"
]
}, },
"gid": 1000, "gid": 1000,
"home_dir": "/tmp/test_home_dir", "home_dir": "/tmp/test_home_dir",
@@ -112,7 +115,7 @@ Output:
Command: Command:
``` ```
python sftpgo_api_cli.py update-user 9576 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --subdirs-permissions "/dir1:list,download,create_symlinks" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date "" --allowed-ip "" --denied-ip "192.168.1.0/24" --fs local python sftpgo_api_cli.py update-user 9576 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --subdirs-permissions "/dir1:list,download,create_symlinks" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date "" --allowed-ip "" --denied-ip "192.168.1.0/24" --denied-login-methods "" --fs local
``` ```
Output: Output:
@@ -145,7 +148,6 @@ Output:
"s3config": {} "s3config": {}
}, },
"filters": { "filters": {
"allowed_ip": [],
"denied_ip": [ "denied_ip": [
"192.168.1.0/24" "192.168.1.0/24"
] ]
@@ -198,7 +200,6 @@ Output:
"s3config": {} "s3config": {}
}, },
"filters": { "filters": {
"allowed_ip": [],
"denied_ip": [ "denied_ip": [
"192.168.1.0/24" "192.168.1.0/24"
] ]

View File

@@ -76,7 +76,7 @@ class SFTPGoApiRequests:
status=1, expiration_date=0, allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', status=1, expiration_date=0, allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='',
s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='',
s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='', s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='',
gcs_automatic_credentials='automatic'): gcs_automatic_credentials='automatic', denied_login_methods=[]):
user = {'id':user_id, 'username':username, 'uid':uid, 'gid':gid, user = {'id':user_id, 'username':username, 'uid':uid, 'gid':gid,
'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, 'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files,
'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth, 'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth,
@@ -92,8 +92,8 @@ class SFTPGoApiRequests:
user.update({'home_dir':home_dir}) user.update({'home_dir':home_dir})
if permissions: if permissions:
user.update({'permissions':permissions}) user.update({'permissions':permissions})
if allowed_ip or denied_ip: if allowed_ip or denied_ip or denied_login_methods:
user.update({'filters':self.buildFilters(allowed_ip, denied_ip)}) user.update({'filters':self.buildFilters(allowed_ip, denied_ip, denied_login_methods)})
user.update({'filesystem':self.buildFsConfig(fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, user.update({'filesystem':self.buildFsConfig(fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret,
s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket,
gcs_key_prefix, gcs_storage_class, gcs_credentials_file, gcs_key_prefix, gcs_storage_class, gcs_credentials_file,
@@ -117,7 +117,7 @@ class SFTPGoApiRequests:
permissions.update({directory:values}) permissions.update({directory:values})
return permissions return permissions
def buildFilters(self, allowed_ip, denied_ip): def buildFilters(self, allowed_ip, denied_ip, denied_login_methods):
filters = {} filters = {}
if allowed_ip: if allowed_ip:
if len(allowed_ip) == 1 and not allowed_ip[0]: if len(allowed_ip) == 1 and not allowed_ip[0]:
@@ -129,6 +129,11 @@ class SFTPGoApiRequests:
filters.update({'denied_ip':[]}) filters.update({'denied_ip':[]})
else: else:
filters.update({'denied_ip':denied_ip}) filters.update({'denied_ip':denied_ip})
if denied_login_methods:
if len(denied_login_methods) == 1 and not denied_login_methods[0]:
filters.update({'denied_login_methods':[]})
else:
filters.update({'denied_login_methods':denied_login_methods})
return filters return filters
def buildFsConfig(self, fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, s3_endpoint, def buildFsConfig(self, fs_provider, s3_bucket, s3_region, s3_access_key, s3_access_secret, s3_endpoint,
@@ -166,12 +171,13 @@ class SFTPGoApiRequests:
quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1, expiration_date=0, quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1, expiration_date=0,
subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', s3_region='', subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local', s3_bucket='', s3_region='',
s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_key_prefix='', gcs_bucket='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_key_prefix='', gcs_bucket='',
gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='', gcs_automatic_credentials='automatic'): gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='', gcs_automatic_credentials='automatic',
denied_login_methods=[]):
u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions, u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth, quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key, status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key,
s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class, s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class,
gcs_credentials_file, gcs_automatic_credentials) gcs_credentials_file, gcs_automatic_credentials, denied_login_methods)
r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify) r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify)
self.printResponse(r) self.printResponse(r)
@@ -180,12 +186,12 @@ class SFTPGoApiRequests:
expiration_date=0, subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local', expiration_date=0, subdirs_permissions=[], allowed_ip=[], denied_ip=[], fs_provider='local',
s3_bucket='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='', s3_bucket='', s3_region='', s3_access_key='', s3_access_secret='', s3_endpoint='', s3_storage_class='',
s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='', s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='',
gcs_automatic_credentials='automatic'): gcs_automatic_credentials='automatic', denied_login_methods=[]):
u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions, u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth, quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key, status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key,
s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class, s3_access_secret, s3_endpoint, s3_storage_class, s3_key_prefix, gcs_bucket, gcs_key_prefix, gcs_storage_class,
gcs_credentials_file, gcs_automatic_credentials) gcs_credentials_file, gcs_automatic_credentials, denied_login_methods)
r = requests.put(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), json=u, auth=self.auth, verify=self.verify) r = requests.put(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), json=u, auth=self.auth, verify=self.verify)
self.printResponse(r) self.printResponse(r)
@@ -426,6 +432,8 @@ def addCommonUserArguments(parser):
choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs', choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs',
'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=[],
choices=['', 'publickey', 'password', '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('-U', '--upload-bandwidth', type=int, default=0, parser.add_argument('-U', '--upload-bandwidth', type=int, default=0,
@@ -569,7 +577,8 @@ if __name__ == '__main__':
args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date), args.subdirs_permissions, args.allowed_ip, args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date), args.subdirs_permissions, args.allowed_ip,
args.denied_ip, args.fs, args.s3_bucket, args.s3_region, args.s3_access_key, args.s3_access_secret, args.denied_ip, args.fs, args.s3_bucket, args.s3_region, args.s3_access_key, args.s3_access_secret,
args.s3_endpoint, args.s3_storage_class, args.s3_key_prefix, args.gcs_bucket, args.gcs_key_prefix, args.s3_endpoint, args.s3_storage_class, args.s3_key_prefix, args.gcs_bucket, args.gcs_key_prefix,
args.gcs_storage_class, args.gcs_credentials_file, args.gcs_automatic_credentials) args.gcs_storage_class, args.gcs_credentials_file, args.gcs_automatic_credentials,
args.denied_login_methods)
elif args.command == 'update-user': elif args.command == 'update-user':
api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid,
args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth,
@@ -577,7 +586,7 @@ if __name__ == '__main__':
args.subdirs_permissions, args.allowed_ip, args.denied_ip, args.fs, args.s3_bucket, args.s3_region, args.subdirs_permissions, args.allowed_ip, args.denied_ip, args.fs, args.s3_bucket, args.s3_region,
args.s3_access_key, args.s3_access_secret, args.s3_endpoint, args.s3_storage_class, args.s3_access_key, args.s3_access_secret, args.s3_endpoint, args.s3_storage_class,
args.s3_key_prefix, args.gcs_bucket, args.gcs_key_prefix, args.gcs_storage_class, args.s3_key_prefix, args.gcs_bucket, args.gcs_key_prefix, args.gcs_storage_class,
args.gcs_credentials_file, args.gcs_automatic_credentials) args.gcs_credentials_file, args.gcs_automatic_credentials, args.denied_login_methods)
elif args.command == 'delete-user': elif args.command == 'delete-user':
api.deleteUser(args.id) api.deleteUser(args.id)
elif args.command == 'get-users': elif args.command == 'get-users':

View File

@@ -438,7 +438,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, "password", "") _, err := loginUser(u, dataprovider.SSHLoginMethodPassword, "", "")
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

@@ -292,7 +292,7 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
// Unmarshal cannot fails here and even if it fails we'll have a user with no permissions // Unmarshal cannot fails here and even if it fails we'll have a user with no permissions
json.Unmarshal([]byte(sconn.Permissions.Extensions["user"]), &user) json.Unmarshal([]byte(sconn.Permissions.Extensions["user"]), &user)
loginType := sconn.Permissions.Extensions["login_type"] loginType := sconn.Permissions.Extensions["login_method"]
connectionID := hex.EncodeToString(sconn.SessionID()) connectionID := hex.EncodeToString(sconn.SessionID())
fs, err := user.GetFilesystem(connectionID) fs, err := user.GetFilesystem(connectionID)
@@ -391,7 +391,7 @@ func (c Configuration) createHandler(connection Connection) sftp.Handlers {
} }
} }
func loginUser(user dataprovider.User, loginType string, remoteAddr string) (*ssh.Permissions, error) { func loginUser(user dataprovider.User, loginMethod, remoteAddr, publicKey string) (*ssh.Permissions, error) {
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, "", "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)
@@ -405,9 +405,13 @@ func loginUser(user dataprovider.User, loginType string, remoteAddr string) (*ss
return nil, fmt.Errorf("too many open sessions: %v", activeSessions) return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
} }
} }
if !user.IsLoginAllowed(remoteAddr) { if !user.IsLoginMethodAllowed(loginMethod) {
logger.Debug(logSender, "", "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.IsLoginFromAddrAllowed(remoteAddr) {
logger.Debug(logSender, "", "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr) logger.Debug(logSender, "", "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr)
return nil, fmt.Errorf("Login is not allowed from this address: %v", 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)
@@ -415,10 +419,13 @@ func loginUser(user dataprovider.User, loginType string, remoteAddr string) (*ss
logger.Warn(logSender, "", "error serializing user info: %v, authentication rejected", err) logger.Warn(logSender, "", "error serializing user info: %v, authentication rejected", err)
return nil, err return nil, err
} }
if len(publicKey) > 0 {
loginMethod = fmt.Sprintf("%v: %v", loginMethod, publicKey)
}
p := &ssh.Permissions{} p := &ssh.Permissions{}
p.Extensions = make(map[string]string) p.Extensions = make(map[string]string)
p.Extensions["user"] = string(json) p.Extensions["user"] = string(json)
p.Extensions["login_type"] = loginType p.Extensions["login_method"] = loginMethod
return p, nil return p, nil
} }
@@ -472,11 +479,12 @@ func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKe
var keyID string var keyID string
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
method := "public_key" method := dataprovider.SSHLoginMethodPublicKey
metrics.AddLoginAttempt(method) 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, fmt.Sprintf("%v:%v", method, keyID), conn.RemoteAddr().String()) sshPerm, err = loginUser(user, method, conn.RemoteAddr().String(), keyID)
} else { }
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())
} }
metrics.AddLoginResult(method, err) metrics.AddLoginResult(method, err)
@@ -488,11 +496,12 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
var user dataprovider.User var user dataprovider.User
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
method := "password" method := dataprovider.SSHLoginMethodPassword
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.RemoteAddr().String(), "")
} else { }
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())
} }
metrics.AddLoginResult(method, err) metrics.AddLoginResult(method, err)
@@ -504,11 +513,12 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
var user dataprovider.User var user dataprovider.User
var sshPerm *ssh.Permissions var sshPerm *ssh.Permissions
method := "keyboard-interactive" method := dataprovider.SSHLoginMethodKeyboardInteractive
metrics.AddLoginAttempt(method) metrics.AddLoginAttempt(method)
if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveProgram, client); err == nil { if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveProgram, client); err == nil {
sshPerm, err = loginUser(user, method, conn.RemoteAddr().String()) sshPerm, err = loginUser(user, method, conn.RemoteAddr().String(), "")
} else { }
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())
} }
metrics.AddLoginResult(method, err) metrics.AddLoginResult(method, err)

View File

@@ -1072,6 +1072,55 @@ func TestLoginInvalidFs(t *testing.T) {
os.RemoveAll(user.GetHomeDir()) os.RemoveAll(user.GetHomeDir())
} }
func TestDeniedLoginMethods(t *testing.T) {
u := getTestUser(true)
u.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword}
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("public key login is disabled, authentication must fail")
}
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPassword}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to update user: %v", err)
}
client, err := getSftpClient(user, true)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
client.Close()
}
user.Password = defaultPassword
user, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to update user: %v", err)
}
_, err = getSftpClient(user, false)
if err == nil {
t.Error("password login is disabled, authentication must fail")
}
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPublicKey}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to update user: %v", err)
}
client, err = getSftpClient(user, false)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
client.Close()
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
os.RemoveAll(user.GetHomeDir())
}
func TestLoginWithIPFilters(t *testing.T) { func TestLoginWithIPFilters(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
@@ -3264,53 +3313,53 @@ func TestUserEmptySubDirPerms(t *testing.T) {
func TestUserFiltersIPMaskConditions(t *testing.T) { func TestUserFiltersIPMaskConditions(t *testing.T) {
user := getTestUser(true) user := getTestUser(true)
// with no filter login must be allowed even if the remoteIP is invalid // with no filter login must be allowed even if the remoteIP is invalid
if !user.IsLoginAllowed("192.168.1.5") { if !user.IsLoginFromAddrAllowed("192.168.1.5") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
if !user.IsLoginAllowed("invalid") { if !user.IsLoginFromAddrAllowed("invalid") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
user.Filters.DeniedIP = append(user.Filters.DeniedIP, "192.168.1.0/24") user.Filters.DeniedIP = append(user.Filters.DeniedIP, "192.168.1.0/24")
if user.IsLoginAllowed("192.168.1.5") { if user.IsLoginFromAddrAllowed("192.168.1.5") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
if !user.IsLoginAllowed("192.168.2.6") { if !user.IsLoginFromAddrAllowed("192.168.2.6") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
user.Filters.AllowedIP = append(user.Filters.AllowedIP, "192.168.1.5/32") user.Filters.AllowedIP = append(user.Filters.AllowedIP, "192.168.1.5/32")
// if the same ip/mask is both denied and allowed then login must be denied // if the same ip/mask is both denied and allowed then login must be denied
if user.IsLoginAllowed("192.168.1.5") { if user.IsLoginFromAddrAllowed("192.168.1.5") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
if user.IsLoginAllowed("192.168.3.6") { if user.IsLoginFromAddrAllowed("192.168.3.6") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
user.Filters.DeniedIP = []string{} user.Filters.DeniedIP = []string{}
if !user.IsLoginAllowed("192.168.1.5") { if !user.IsLoginFromAddrAllowed("192.168.1.5") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
if user.IsLoginAllowed("192.168.1.6") { if user.IsLoginFromAddrAllowed("192.168.1.6") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
user.Filters.DeniedIP = []string{"192.168.0.0/16", "172.16.0.0/16"} user.Filters.DeniedIP = []string{"192.168.0.0/16", "172.16.0.0/16"}
user.Filters.AllowedIP = []string{} user.Filters.AllowedIP = []string{}
if user.IsLoginAllowed("192.168.5.255") { if user.IsLoginFromAddrAllowed("192.168.5.255") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
if user.IsLoginAllowed("172.16.1.2") { if user.IsLoginFromAddrAllowed("172.16.1.2") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
if !user.IsLoginAllowed("172.18.2.1") { if !user.IsLoginFromAddrAllowed("172.18.2.1") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
user.Filters.AllowedIP = []string{"10.4.4.0/24"} user.Filters.AllowedIP = []string{"10.4.4.0/24"}
if user.IsLoginAllowed("10.5.4.2") { if user.IsLoginFromAddrAllowed("10.5.4.2") {
t.Error("unexpected login allowed") t.Error("unexpected login allowed")
} }
if !user.IsLoginAllowed("10.4.4.2") { if !user.IsLoginFromAddrAllowed("10.4.4.2") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
if !user.IsLoginAllowed("invalid") { if !user.IsLoginFromAddrAllowed("invalid") {
t.Error("unexpected login denied") t.Error("unexpected login denied")
} }
} }

View File

@@ -70,6 +70,19 @@
</div> </div>
</div> </div>
<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>
{{range $method := .ValidSSHLoginMethods}}
<option value="{{$method}}"
{{range $m := $.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label> <label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
<div class="col-sm-10"> <div class="col-sm-10">