diff --git a/README.md b/README.md index 127ac166..3056d9c7 100644 --- a/README.md +++ b/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. 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. +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: -- 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 +- 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 program will not be used for public key 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. @@ -360,6 +360,8 @@ else 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 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. -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 - 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 diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 1ba29679..f83122b8 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -166,10 +166,11 @@ type Config struct { // 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 defines the scope for the external authentication program. + // - 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 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"` } @@ -722,7 +723,7 @@ func doExternalAuth(username, password, pubKey string) (User, error) { return user, errors.New("Invalid credentials") } 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) } u, err := provider.userExists(username) @@ -771,7 +772,7 @@ func executeAction(operation string, user User) { config.Actions.Command, commandArgs, err) if err == nil { // 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() } } else { diff --git a/sftpd/handler.go b/sftpd/handler.go index 42482536..8b0db425 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -513,15 +513,15 @@ func (c Connection) hasSpace(checkFiles bool) bool { numFile, size, err := dataprovider.GetUsedQuota(dataProvider, c.User.Username) if err != nil { 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 } - 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 } if (checkFiles && c.User.QuotaFiles > 0 && numFile >= c.User.QuotaFiles) || (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) return false } diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index 78ff48c1..a92da90e 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -428,7 +428,7 @@ func executeAction(operation, username, path, target, sshCmd string) error { actions.Command, operation, username, path, target, sshCmd, err) if err == nil { // 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() } } else { diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 580687d1..02278491 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -1155,7 +1155,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { providerConf.ExternalAuthScope = 0 err := dataprovider.Initialize(providerConf, configDir) if err != nil { - t.Errorf("error initializing data provider with users base dir") + t.Errorf("error initializing data provider") } httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) @@ -1176,6 +1176,7 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) { if err != nil { t.Errorf("file upload error: %v", err) } + os.Remove(testFilePath) } u.Username = defaultUsername + "1" client, err = getSftpClient(u, usePubKey) @@ -1244,7 +1245,7 @@ func TestLoginExternalAuthPwd(t *testing.T) { providerConf.ExternalAuthScope = 1 err := dataprovider.Initialize(providerConf, configDir) if err != nil { - t.Errorf("error initializing data provider with users base dir") + t.Errorf("error initializing data provider") } httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) @@ -1312,7 +1313,7 @@ func TestLoginExternalAuthPubKey(t *testing.T) { providerConf.ExternalAuthScope = 2 err := dataprovider.Initialize(providerConf, configDir) if err != nil { - t.Errorf("error initializing data provider with users base dir") + t.Errorf("error initializing data provider") } httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) @@ -1380,7 +1381,7 @@ func TestLoginExternalAuthErrors(t *testing.T) { providerConf.ExternalAuthScope = 0 err := dataprovider.Initialize(providerConf, configDir) if err != nil { - t.Errorf("error initializing data provider with users base dir") + t.Errorf("error initializing data provider") } httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) @@ -1416,6 +1417,61 @@ func TestLoginExternalAuthErrors(t *testing.T) { 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) { usePubKey := false u := getTestUser(usePubKey)