add support for authentication using external programs

Fixes #62
This commit is contained in:
Nicola Murino 2020-01-06 21:42:41 +01:00
parent e046b35b97
commit 531091906d
6 changed files with 485 additions and 5 deletions

View file

@ -9,6 +9,7 @@ Full featured and highly configurable SFTP server
- SFTP accounts are virtual accounts stored in a "data provider". - SFTP accounts are virtual accounts stored in a "data provider".
- 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.
- 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.
- Per user maximum concurrent sessions. - Per user maximum concurrent sessions.
@ -194,6 +195,8 @@ The `sftpgo` configuration file contains the following sections:
- `uid` - `uid`
- `gid` - `gid`
- `http_notification_url`, a valid URL. The action is added to the query string. For example `<http_notification_url>?action=update`. An HTTP POST request will be executed to this URL. The user is sent serialized as json inside the POST body. Leave empty to disable. - `http_notification_url`, a valid URL. The action is added to the query string. For example `<http_notification_url>?action=update`. An HTTP POST request will be executed to this URL. The user is sent serialized as json inside the POST body. Leave empty to disable.
- `external_auth_program`, string. Absolute path to an external program to use for users authentication. See the "External Authentication" paragraph for more details.
- `external_auth_scope`, integer. 0 means all supported authetication scopes (passwords and public keys). 1 means passwords only. 2 means public keys only
- **"httpd"**, the configuration for the HTTP server used to serve REST API - **"httpd"**, the configuration for the HTTP server used to serve REST API
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080 - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1" - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
@ -245,7 +248,9 @@ Here is a full example showing the default config in JSON format:
"execute_on": [], "execute_on": [],
"command": "", "command": "",
"http_notification_url": "" "http_notification_url": ""
} },
"external_auth_program": "",
"external_auth_scope": 0
}, },
"httpd": { "httpd": {
"bind_port": 8080, "bind_port": 8080,
@ -319,6 +324,42 @@ netsh advfirewall firewall add rule name="SFTPGo Service" dir=in action=allow pr
or through the Windows Firewall GUI. or through the Windows Firewall GUI.
## External Authentication
Custom authentication methods can easily be added. SFTPGo supports external authentication modules, and writing a new backend can be as simple as a few lines of shell script.
To enable external authentication you must set the absolute path of your authentication program using `external_auth_program` key in your configuration file.
The external program can read the following environment variables to get info about the user trying to authenticate:
- `SFTPGO_AUTHD_USERNAME`
- `SFTPGO_AUTHD_PASSWORD`, not empty for password authentication
- `SFTPGO_AUTHD_PUBLIC_KEY`, not empty for public key authentication
The content of these variables is _not_ quoted. They may contain special characters. They are under the control of a possibly malicious remote user.
The program must respond on the standard output with a valid SFTPGo user serialized as json if the authentication succeed or an user with an empty username if the authentication fails.
If the authentication succeed the user will be automatically added/updated inside the defined data provider. Actions defined for user added/updated will not be executed in this case.
The external program should check authentication only, if there are login restrictions such as user disabled, expired, login allowed only from specific IP addresses it is enough to populate the matching user fields and these conditions will be checked in the same way as for built-in users.
The external auth program must finish within 15 seconds.
This method is slower than built-in authentication methods, but it's very flexible as anyone can easily write his own authentication programs.
You can also restrict the authentication scope for the external program using the `external_auth_scope` configuration key:
- 0 means all supported authetication scopes, both password and public keys
- 1 means passwords only, the external auth program will not be used for public key authentication
- 2 means public keys only, the external auth program will not be used for password authentication
Let's see a very basic example. Our sample authentication program will only accept user `test_user` with any password or public key.
```
#!/bin/sh
if test "$SFTPGO_AUTHD_USERNAME" = "test_user"; then
echo '{"status":1,"username":"test_user","expiration_date":0,"home_dir":"/tmp/test_user","uid":0,"gid":0,"max_sessions":0,"quota_size":0,"quota_files":100000,"permissions":{"/":["*"],"/somedir":["list","download"]},"upload_bandwidth":0,"download_bandwidth":0,"filters":{"allowed_ip":[],"denied_ip":[]},"public_keys":[]}'
else
echo '{"username":""}'
fi
```
## Portable mode ## Portable mode
SFTPGo allows to share a single directory on demand using the `portable` subcommand: SFTPGo allows to share a single directory on demand using the `portable` subcommand:
@ -455,7 +496,7 @@ Please check the `/metrics` page for more details.
## Web Admin ## Web Admin
You can easily build your own interface using the exposed REST API, anyway SFTPGo provides also a very basic builtin web interface that allows to manage users and connections. You can easily build your own interface using the exposed REST API, anyway SFTPGo provides also a very basic built-in web interface that allows to manage users and connections.
With the default `httpd` configuration, the web admin is available at the following URL: With the default `httpd` configuration, the web admin is available at the following URL:
[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web) [http://127.0.0.1:8080/web](http://127.0.0.1:8080/web)

View file

@ -81,6 +81,8 @@ func init() {
Command: "", Command: "",
HTTPNotificationURL: "", HTTPNotificationURL: "",
}, },
ExternalAuthProgram: "",
ExternalAuthScope: 0,
}, },
HTTPDConfig: httpd.Conf{ HTTPDConfig: httpd.Conf{
BindPort: 8080, BindPort: 8080,
@ -169,6 +171,12 @@ func LoadConfig(configDir, configName string) error {
logger.Warn(logSender, "", "Configuration error: %v", err) logger.Warn(logSender, "", "Configuration error: %v", err)
logger.WarnToConsole("Configuration error: %v", err) logger.WarnToConsole("Configuration error: %v", err)
} }
if globalConf.ProviderConf.ExternalAuthScope < 0 || globalConf.ProviderConf.ExternalAuthScope > 2 {
err = fmt.Errorf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope)
globalConf.ProviderConf.ExternalAuthScope = 0
logger.Warn(logSender, "", "Configuration error: %v", err)
logger.WarnToConsole("Configuration error: %v", err)
}
logger.Debug(logSender, "", "config file used: '%v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf()) logger.Debug(logSender, "", "config file used: '%v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf())
return err return err
} }

View file

@ -98,6 +98,27 @@ func TestInvalidUploadMode(t *testing.T) {
os.Remove(configFilePath) os.Remove(configFilePath)
} }
func TestInvalidExternalAuthScope(t *testing.T) {
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
config.LoadConfig(configDir, "")
providerConf := config.GetProviderConf()
providerConf.ExternalAuthScope = 10
c := make(map[string]dataprovider.Config)
c["data_provider"] = providerConf
jsonConf, _ := json.Marshal(c)
err := ioutil.WriteFile(configFilePath, jsonConf, 0666)
if err != nil {
t.Errorf("error saving temporary configuration")
}
err = config.LoadConfig(configDir, tempConfigName)
if err == nil {
t.Errorf("Loading configuration with invalid external_auth_scope must fail")
}
os.Remove(configFilePath)
}
func TestSetGetConfig(t *testing.T) { func TestSetGetConfig(t *testing.T) {
sftpdConf := config.GetSFTPDConfig() sftpdConf := config.GetSFTPDConfig()
sftpdConf.IdleTimeout = 3 sftpdConf.IdleTimeout = 3

View file

@ -5,6 +5,7 @@ package dataprovider
import ( import (
"bytes" "bytes"
"context"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
@ -100,7 +101,8 @@ type Actions struct {
type Config struct { type Config struct {
// Driver name, must be one of the SupportedProviders // Driver name, must be one of the SupportedProviders
Driver string `json:"driver" mapstructure:"driver"` Driver string `json:"driver" mapstructure:"driver"`
// Database name // Database name. For driver sqlite this can be the database name relative to the config dir
// or the absolute path to the SQLite database.
Name string `json:"name" mapstructure:"name"` Name string `json:"name" mapstructure:"name"`
// Database host // Database host
Host string `json:"host" mapstructure:"host"` Host string `json:"host" mapstructure:"host"`
@ -141,6 +143,34 @@ type Config struct {
// Actions to execute on user add, update, delete. // Actions to execute on user add, update, delete.
// Update action will not be fired for internal updates such as the last login or the user quota fields. // Update action will not be fired for internal updates such as the last login or the user quota fields.
Actions Actions `json:"actions" mapstructure:"actions"` Actions Actions `json:"actions" mapstructure:"actions"`
// Absolute path to an external program to use for users authentication. Leave empty to use builtin
// authentication.
// The external program can read the following environment variables to get info about the user trying
// to authenticate:
//
// - SFTPGO_AUTHD_USERNAME
// - SFTPGO_AUTHD_PASSWORD, not empty for password authentication
// - SFTPGO_AUTHD_PUBLIC_KEY, not empty for public key authentication
//
// The content of these variables is _not_ quoted. They may contain special characters. They are under the
// control of a possibly malicious remote user.
//
// The program must respond on the standard output with a valid SFTPGo user serialized as json if the
// authentication succeed or an user with an empty username if the authentication fails.
// If the authentication succeed the user will be automatically added/updated inside the defined data provider.
// Actions defined for user added/updated will not be executed in this case.
// The external program should check authentication only, if there are login restrictions such as user
// disabled, expired, login allowed only from specific IP addresses it is enough to populate the matching user
// fields and these conditions will be checked in the same way as for builtin users.
// The external auth program must finish within 15 seconds.
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
// easily write his own authentication programs.
ExternalAuthProgram string `json:"external_auth_program" mapstructure:"external_auth_program"`
// defines the scope for the external auth program, if defined.
// 0 means all supported authetication scopes, both password and public keys
// 1 means passwords only, the external auth program will not be used for public key authentication
// 2 means public keys only, the external auth program will not be used for password authentication
ExternalAuthScope int `json:"external_auth_scope" mapstructure:"external_auth_scope"`
} }
// ValidationError raised if input data is not valid // ValidationError raised if input data is not valid
@ -212,6 +242,18 @@ func Initialize(cnf Config, basePath string) error {
var err error var err error
config = cnf config = cnf
sqlPlaceholders = getSQLPlaceholders() sqlPlaceholders = getSQLPlaceholders()
if len(config.ExternalAuthProgram) > 0 {
if !filepath.IsAbs(config.ExternalAuthProgram) {
return fmt.Errorf("invalid external auth program: %#v must be an absolute path", config.ExternalAuthProgram)
}
_, err := os.Stat(config.ExternalAuthProgram)
if err != nil {
providerLog(logger.LevelWarn, "invalid external auth program:: %v", err)
return err
}
}
if config.Driver == SQLiteDataProviderName { if config.Driver == SQLiteDataProviderName {
err = initializeSQLiteProvider(basePath) err = initializeSQLiteProvider(basePath)
} else if config.Driver == PGSQLDataProviderName { } else if config.Driver == PGSQLDataProviderName {
@ -233,11 +275,25 @@ func Initialize(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.ExternalAuthProgram) > 0 && config.ExternalAuthScope <= 1 {
user, err := doExternalAuth(username, password, "")
if err != nil {
return user, err
}
return checkUserAndPass(user, password)
}
return p.validateUserAndPass(username, password) return p.validateUserAndPass(username, password)
} }
// 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 string) (User, string, error) {
if len(config.ExternalAuthProgram) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope == 2) {
user, err := doExternalAuth(username, "", pubKey)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(user, pubKey)
}
return p.validateUserAndPubKey(username, pubKey) return p.validateUserAndPubKey(username, pubKey)
} }
@ -636,6 +692,56 @@ func checkDataprovider() {
metrics.UpdateDataProviderAvailability(err) metrics.UpdateDataProviderAvailability(err)
} }
func doExternalAuth(username, password, pubKey string) (User, error) {
var user User
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, config.ExternalAuthProgram)
cmd.Env = append(cmd.Env, fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username))
if len(password) > 0 {
cmd.Env = append(cmd.Env, fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password))
}
pkey := ""
if len(pubKey) > 0 {
k, err := ssh.ParsePublicKey([]byte(pubKey))
if err != nil {
return user, err
}
pkey = string(ssh.MarshalAuthorizedKey(k))
cmd.Env = append(cmd.Env, fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey))
}
out, err := cmd.Output()
if err != nil {
return user, fmt.Errorf("External auth error: %v env: %+v", err, cmd.Env)
}
err = json.Unmarshal(out, &user)
if err != nil {
return user, fmt.Errorf("Invalid external auth response: %v", err)
}
if len(user.Username) == 0 {
return user, errors.New("Invalid credentials")
}
user.Password = password
if len(pkey) > 0 && !utils.IsStringInSlice(pkey, user.PublicKeys) {
user.PublicKeys = append(user.PublicKeys, pkey)
}
u, err := provider.userExists(username)
if err == nil {
user.ID = u.ID
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastQuotaUpdate = u.LastQuotaUpdate
user.LastLogin = u.LastLogin
err = provider.updateUser(user)
} else {
err = provider.addUser(user)
}
if err != nil {
return user, err
}
return provider.userExists(username)
}
func providerLog(level logger.LogLevel, format string, v ...interface{}) { func providerLog(level logger.LogLevel, format string, v ...interface{}) {
logger.Log(level, logSender, "", format, v...) logger.Log(level, logSender, "", format, v...)
} }

View file

@ -6,6 +6,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/json"
"fmt" "fmt"
"hash" "hash"
"io" "io"
@ -95,6 +96,7 @@ var (
pubKeyPath string pubKeyPath string
privateKeyPath string privateKeyPath string
gitWrapPath string gitWrapPath string
extAuthPath string
logFilePath string logFilePath string
) )
@ -163,6 +165,7 @@ func TestMain(m *testing.M) {
pubKeyPath = filepath.Join(homeBasePath, "ssh_key.pub") pubKeyPath = filepath.Join(homeBasePath, "ssh_key.pub")
privateKeyPath = filepath.Join(homeBasePath, "ssh_key") privateKeyPath = filepath.Join(homeBasePath, "ssh_key")
gitWrapPath = filepath.Join(homeBasePath, "gitwrap.sh") gitWrapPath = filepath.Join(homeBasePath, "gitwrap.sh")
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
err = ioutil.WriteFile(pubKeyPath, []byte(testPubKey+"\n"), 0600) err = ioutil.WriteFile(pubKeyPath, []byte(testPubKey+"\n"), 0600)
if err != nil { if err != nil {
logger.WarnToConsole("unable to save public key to file: %v", err) logger.WarnToConsole("unable to save public key to file: %v", err)
@ -176,7 +179,6 @@ func TestMain(m *testing.M) {
if err != nil { if err != nil {
logger.WarnToConsole("unable to save gitwrap shell script: %v", err) logger.WarnToConsole("unable to save gitwrap shell script: %v", err)
} }
sftpd.SetDataProvider(dataProvider) sftpd.SetDataProvider(dataProvider)
httpd.SetDataProvider(dataProvider) httpd.SetDataProvider(dataProvider)
@ -202,6 +204,7 @@ func TestMain(m *testing.M) {
os.Remove(pubKeyPath) os.Remove(pubKeyPath)
os.Remove(privateKeyPath) os.Remove(privateKeyPath)
os.Remove(gitWrapPath) os.Remove(gitWrapPath)
os.Remove(extAuthPath)
os.Exit(exitCode) os.Exit(exitCode)
} }
@ -1136,6 +1139,283 @@ func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
os.RemoveAll(user.GetHomeDir()) os.RemoveAll(user.GetHomeDir())
} }
func TestLoginExternalAuthPwdAndPubKey(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this test is not available on Windows")
}
usePubKey := true
u := getTestUser(usePubKey)
u.QuotaFiles = 1000
dataProvider := dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf := config.GetProviderConf()
ioutil.WriteFile(extAuthPath, getExtAuthScriptContent(u, 0, false), 0755)
providerConf.ExternalAuthProgram = extAuthPath
providerConf.ExternalAuthScope = 0
err := dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider with users base dir")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
client, err := getSftpClient(u, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
testFileName := "test_file.dat"
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if err != nil {
t.Errorf("file upload error: %v", err)
}
}
u.Username = defaultUsername + "1"
client, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("external auth login with invalid user must fail")
}
usePubKey = false
u = getTestUser(usePubKey)
u.PublicKeys = []string{}
ioutil.WriteFile(extAuthPath, getExtAuthScriptContent(u, 0, false), 0755)
client, err = getSftpClient(u, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err := client.Getwd()
if err != nil {
t.Errorf("unable to get working dir: %v", err)
}
}
users, out, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
if err != nil {
t.Errorf("unable to get users: %v, out: %v", err, string(out))
}
if len(users) != 1 {
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
}
user := users[0]
if len(user.PublicKeys) != 0 {
t.Errorf("number of public keys mismatch, expected: 0, actual: %v", len(user.PublicKeys))
}
if user.UsedQuotaSize == 0 {
t.Error("quota size must be > 0")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove: %v", err)
}
os.RemoveAll(user.GetHomeDir())
dataProvider = dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
os.Remove(extAuthPath)
}
func TestLoginExternalAuthPwd(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this test is not available on Windows")
}
usePubKey := false
u := getTestUser(usePubKey)
dataProvider := dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf := config.GetProviderConf()
ioutil.WriteFile(extAuthPath, getExtAuthScriptContent(u, 0, false), 0755)
providerConf.ExternalAuthProgram = extAuthPath
providerConf.ExternalAuthScope = 1
err := dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider with users base dir")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
client, err := getSftpClient(u, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err := client.Getwd()
if err != nil {
t.Errorf("unable to get working dir: %v", err)
}
}
u.Username = defaultUsername + "1"
client, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("external auth login with invalid user must fail")
}
usePubKey = true
u = getTestUser(usePubKey)
client, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("external auth login with valid user but invalid auth scope must fail")
}
users, out, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
if err != nil {
t.Errorf("unable to get users: %v, out: %v", err, string(out))
}
if len(users) != 1 {
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
}
user := users[0]
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove: %v", err)
}
os.RemoveAll(user.GetHomeDir())
dataProvider = dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
os.Remove(extAuthPath)
}
func TestLoginExternalAuthPubKey(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this test is not available on Windows")
}
usePubKey := true
u := getTestUser(usePubKey)
dataProvider := dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf := config.GetProviderConf()
ioutil.WriteFile(extAuthPath, getExtAuthScriptContent(u, 0, false), 0755)
providerConf.ExternalAuthProgram = extAuthPath
providerConf.ExternalAuthScope = 2
err := dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider with users base dir")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
client, err := getSftpClient(u, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err := client.Getwd()
if err != nil {
t.Errorf("unable to get working dir: %v", err)
}
}
u.Username = defaultUsername + "1"
client, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("external auth login with invalid user must fail")
}
usePubKey = false
u = getTestUser(usePubKey)
client, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("external auth login with valid user but invalid auth scope must fail")
}
users, out, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
if err != nil {
t.Errorf("unable to get users: %v, out: %v", err, string(out))
}
if len(users) != 1 {
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
}
user := users[0]
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove: %v", err)
}
os.RemoveAll(user.GetHomeDir())
dataProvider = dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
os.Remove(extAuthPath)
}
func TestLoginExternalAuthErrors(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("this test is not available on Windows")
}
usePubKey := true
u := getTestUser(usePubKey)
dataProvider := dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf := config.GetProviderConf()
ioutil.WriteFile(extAuthPath, getExtAuthScriptContent(u, 0, true), 0755)
providerConf.ExternalAuthProgram = extAuthPath
providerConf.ExternalAuthScope = 0
err := dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider with users base dir")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
_, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("login must fail, external auth returns a non json response")
}
usePubKey = false
u = getTestUser(usePubKey)
_, err = getSftpClient(u, usePubKey)
if err == nil {
t.Error("login must fail, external auth returns a non json response")
}
users, out, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
if err != nil {
t.Errorf("unable to get users: %v, out: %v", err, string(out))
}
if len(users) != 0 {
t.Errorf("number of users mismatch, expected: 0, actual: %v", len(users))
}
dataProvider = dataprovider.GetProvider()
dataprovider.Close(dataProvider)
config.LoadConfig(configDir, "")
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
if err != nil {
t.Errorf("error initializing data provider")
}
httpd.SetDataProvider(dataprovider.GetProvider())
sftpd.SetDataProvider(dataprovider.GetProvider())
os.Remove(extAuthPath)
}
func TestMaxSessions(t *testing.T) { func TestMaxSessions(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
@ -4031,6 +4311,28 @@ func addFileToGitRepo(repoPath string, fileSize int64) ([]byte, error) {
return cmd.CombinedOutput() return cmd.CombinedOutput()
} }
func getExtAuthScriptContent(user dataprovider.User, sleepTime int, nonJsonResponse bool) []byte {
extAuthContent := []byte("#!/bin/sh\n\n")
u, _ := json.Marshal(user)
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
if nonJsonResponse {
extAuthContent = append(extAuthContent, []byte("echo 'text response'\n")...)
} else {
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("echo '%v'\n", string(u)))...)
}
extAuthContent = append(extAuthContent, []byte("else\n")...)
if nonJsonResponse {
extAuthContent = append(extAuthContent, []byte("echo 'text response'\n")...)
} else {
extAuthContent = append(extAuthContent, []byte("echo '{\"username\":\"\"}'\n")...)
}
extAuthContent = append(extAuthContent, []byte("fi\n")...)
if sleepTime > 0 {
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("sleep %v\n", sleepTime))...)
}
return extAuthContent
}
func printLatestLogs(maxNumberOfLines int) { func printLatestLogs(maxNumberOfLines int) {
var lines []string var lines []string
f, err := os.Open(logFilePath) f, err := os.Open(logFilePath)

View file

@ -39,7 +39,9 @@
"execute_on": [], "execute_on": [],
"command": "", "command": "",
"http_notification_url": "" "http_notification_url": ""
} },
"external_auth_program": "",
"external_auth_scope": 0
}, },
"httpd": { "httpd": {
"bind_port": 8080, "bind_port": 8080,