improve docs and test cases
This commit is contained in:
parent
531091906d
commit
8e604f888a
5 changed files with 80 additions and 19 deletions
14
README.md
14
README.md
|
@ -341,12 +341,12 @@ The program must respond on the standard output with a valid SFTPGo user seriali
|
||||||
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.
|
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 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.
|
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.
|
This method is slower than built-in authentication, but it's very flexible as anyone can easily write his own authentication program.
|
||||||
You can also restrict the authentication scope for the external program using the `external_auth_scope` configuration key:
|
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
|
- 0 means all supported authetication scopes, the external program will be used for both password and public key authentication
|
||||||
- 1 means passwords only, the external auth program will not be used for public key authentication
|
- 1 means passwords only, the external 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
|
- 2 means public keys only, the external 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.
|
Let's see a very basic example. Our sample authentication program will only accept user `test_user` with any password or public key.
|
||||||
|
|
||||||
|
@ -360,6 +360,8 @@ else
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you have an external authentication program that could be useful for others too, for example LDAP/Active Directory authentication, please let us know and/or send a pull request.
|
||||||
|
|
||||||
## 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:
|
||||||
|
@ -435,9 +437,11 @@ For each account the following properties can be configured:
|
||||||
|
|
||||||
These properties are stored inside the data provider.
|
These properties are stored inside the data provider.
|
||||||
|
|
||||||
If you want to use your existing accounts you have two options:
|
If you want to use your existing accounts you have these options:
|
||||||
|
|
||||||
- If your accounts are aleady stored inside a supported database, you can create a database view. Since a view is read only, you have to disable user management and quota tracking so SFTPGo will never try to write to the view
|
- If your accounts are aleady stored inside a supported database, you can create a database view. Since a view is read only, you have to disable user management and quota tracking so SFTPGo will never try to write to the view
|
||||||
- you can import your users inside SFTPGo. Take a look at [sftpgo_api_cli.py](./scripts/README.md "sftpgo_api_cli script"), it can convert and import users from Unix system users and Pure-FTPd/ProFTPD virtual users
|
- you can import your users inside SFTPGo. Take a look at [sftpgo_api_cli.py](./scripts/README.md "sftpgo_api_cli script"), it can convert and import users from Unix system users and Pure-FTPd/ProFTPD virtual users
|
||||||
|
- you can use an external authentication program
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
|
|
|
@ -166,10 +166,11 @@ type Config struct {
|
||||||
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
|
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
|
||||||
// easily write his own authentication programs.
|
// easily write his own authentication programs.
|
||||||
ExternalAuthProgram string `json:"external_auth_program" mapstructure:"external_auth_program"`
|
ExternalAuthProgram string `json:"external_auth_program" mapstructure:"external_auth_program"`
|
||||||
// defines the scope for the external auth program, if defined.
|
// ExternalAuthScope defines the scope for the external authentication program.
|
||||||
// 0 means all supported authetication scopes, both password and public keys
|
// - 0 means all supported authetication scopes, the external program will be used for both password and
|
||||||
// 1 means passwords only, the external auth program will not be used for public key authentication
|
// public key authentication
|
||||||
// 2 means public keys only, the external auth program will not be used for password authentication
|
// - 1 means passwords only, the external program will not be used for public key authentication
|
||||||
|
// - 2 means public keys only, the external program will not be used for password authentication
|
||||||
ExternalAuthScope int `json:"external_auth_scope" mapstructure:"external_auth_scope"`
|
ExternalAuthScope int `json:"external_auth_scope" mapstructure:"external_auth_scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,7 +723,7 @@ func doExternalAuth(username, password, pubKey string) (User, error) {
|
||||||
return user, errors.New("Invalid credentials")
|
return user, errors.New("Invalid credentials")
|
||||||
}
|
}
|
||||||
user.Password = password
|
user.Password = password
|
||||||
if len(pkey) > 0 && !utils.IsStringInSlice(pkey, user.PublicKeys) {
|
if len(pkey) > 0 && !utils.IsStringPrefixInSlice(pkey, user.PublicKeys) {
|
||||||
user.PublicKeys = append(user.PublicKeys, pkey)
|
user.PublicKeys = append(user.PublicKeys, pkey)
|
||||||
}
|
}
|
||||||
u, err := provider.userExists(username)
|
u, err := provider.userExists(username)
|
||||||
|
@ -771,7 +772,7 @@ func executeAction(operation string, user User) {
|
||||||
config.Actions.Command, commandArgs, err)
|
config.Actions.Command, commandArgs, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we are in a goroutine but we don't want to block here, this way we can send the
|
// we are in a goroutine but we don't want to block here, this way we can send the
|
||||||
// HTTP notification, if configured, without waiting the end of the command
|
// HTTP notification, if configured, without waiting for the end of the command
|
||||||
go command.Wait()
|
go command.Wait()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -513,15 +513,15 @@ func (c Connection) hasSpace(checkFiles bool) bool {
|
||||||
numFile, size, err := dataprovider.GetUsedQuota(dataProvider, c.User.Username)
|
numFile, size, err := dataprovider.GetUsedQuota(dataProvider, c.User.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*dataprovider.MethodDisabledError); ok {
|
if _, ok := err.(*dataprovider.MethodDisabledError); ok {
|
||||||
c.Log(logger.LevelWarn, logSender, "quota enforcement not possible for user %v: %v", c.User.Username, err)
|
c.Log(logger.LevelWarn, logSender, "quota enforcement not possible for user %#v: %v", c.User.Username, err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
c.Log(logger.LevelWarn, logSender, "error getting used quota for %v: %v", c.User.Username, err)
|
c.Log(logger.LevelWarn, logSender, "error getting used quota for %#v: %v", c.User.Username, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (checkFiles && c.User.QuotaFiles > 0 && numFile >= c.User.QuotaFiles) ||
|
if (checkFiles && c.User.QuotaFiles > 0 && numFile >= c.User.QuotaFiles) ||
|
||||||
(c.User.QuotaSize > 0 && size >= c.User.QuotaSize) {
|
(c.User.QuotaSize > 0 && size >= c.User.QuotaSize) {
|
||||||
c.Log(logger.LevelDebug, logSender, "quota exceed for user %v, num files: %v/%v, size: %v/%v check files: %v",
|
c.Log(logger.LevelDebug, logSender, "quota exceed for user %#v, num files: %v/%v, size: %v/%v check files: %v",
|
||||||
c.User.Username, numFile, c.User.QuotaFiles, size, c.User.QuotaSize, checkFiles)
|
c.User.Username, numFile, c.User.QuotaFiles, size, c.User.QuotaSize, checkFiles)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,7 +428,7 @@ func executeAction(operation, username, path, target, sshCmd string) error {
|
||||||
actions.Command, operation, username, path, target, sshCmd, err)
|
actions.Command, operation, username, path, target, sshCmd, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we are in a goroutine but we don't want to block here, this way we can send the
|
// we are in a goroutine but we don't want to block here, this way we can send the
|
||||||
// HTTP notification, if configured, without waiting the end of the command
|
// HTTP notification, if configured, without waiting for the end of the command
|
||||||
go command.Wait()
|
go command.Wait()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1155,7 +1155,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) {
|
||||||
providerConf.ExternalAuthScope = 0
|
providerConf.ExternalAuthScope = 0
|
||||||
err := dataprovider.Initialize(providerConf, configDir)
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider with users base dir")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
@ -1176,6 +1176,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
|
os.Remove(testFilePath)
|
||||||
}
|
}
|
||||||
u.Username = defaultUsername + "1"
|
u.Username = defaultUsername + "1"
|
||||||
client, err = getSftpClient(u, usePubKey)
|
client, err = getSftpClient(u, usePubKey)
|
||||||
|
@ -1244,7 +1245,7 @@ func TestLoginExternalAuthPwd(t *testing.T) {
|
||||||
providerConf.ExternalAuthScope = 1
|
providerConf.ExternalAuthScope = 1
|
||||||
err := dataprovider.Initialize(providerConf, configDir)
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider with users base dir")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
@ -1312,7 +1313,7 @@ func TestLoginExternalAuthPubKey(t *testing.T) {
|
||||||
providerConf.ExternalAuthScope = 2
|
providerConf.ExternalAuthScope = 2
|
||||||
err := dataprovider.Initialize(providerConf, configDir)
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider with users base dir")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
@ -1380,7 +1381,7 @@ func TestLoginExternalAuthErrors(t *testing.T) {
|
||||||
providerConf.ExternalAuthScope = 0
|
providerConf.ExternalAuthScope = 0
|
||||||
err := dataprovider.Initialize(providerConf, configDir)
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider with users base dir")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
@ -1416,6 +1417,61 @@ func TestLoginExternalAuthErrors(t *testing.T) {
|
||||||
os.Remove(extAuthPath)
|
os.Remove(extAuthPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuotaDisabledError(t *testing.T) {
|
||||||
|
dataProvider := dataprovider.GetProvider()
|
||||||
|
dataprovider.Close(dataProvider)
|
||||||
|
config.LoadConfig(configDir, "")
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
providerConf.TrackQuota = 0
|
||||||
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error initializing data provider")
|
||||||
|
}
|
||||||
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
usePubKey := false
|
||||||
|
u := getTestUser(usePubKey)
|
||||||
|
u.QuotaFiles = 10
|
||||||
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
}
|
||||||
|
client, err := getSftpClient(user, 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)
|
||||||
|
}
|
||||||
|
os.Remove(testFilePath)
|
||||||
|
}
|
||||||
|
_, 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())
|
||||||
|
}
|
||||||
|
|
||||||
func TestMaxSessions(t *testing.T) {
|
func TestMaxSessions(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
|
|
Loading…
Reference in a new issue