REST API/Web admin: add a parameter to disconnect a user after an update

This way you can force the user to login again and so to use the updated
configuration.

A deleted user will be automatically disconnected.

Fixes #163

Improved some docs too.
This commit is contained in:
Nicola Murino 2020-09-01 16:10:26 +02:00
parent dbed110d02
commit 3925c7ff95
20 changed files with 270 additions and 110 deletions

View file

@ -579,7 +579,7 @@ func UpdateUser(user User) error {
return err
}
// DeleteUser deletes an existing SFTP user.
// DeleteUser deletes an existing SFTPGo user.
// ManageUsers configuration must be set to 1 to enable this method
func DeleteUser(user User) error {
if config.ManageUsers == 0 {

View file

@ -18,7 +18,7 @@ The program must write, on its the standard output:
If the hook is an HTTP URL then it will be invoked as HTTP POST. The login method, the used protocol and the ip address of the user trying to login are added to the query string, for example `<http_url>?login_method=password&ip=1.2.3.4&protocol=SSH`.
The request body will contain the user trying to login serialized as JSON. If no modification is needed the HTTP response code must be 204, otherwise the response code must be 200 and the response body a valid SFTPGo user serialized as JSON.
Actions defined for user's updates will not be executed in this case.
Actions defined for user's updates will not be executed in this case and an already logged in user with the same username will not be disconnected, you have to handle these things yourself.
The JSON response can include only the fields to update instead of the full user. For example, if you want to disable the user, you can return a response like this:

View file

@ -25,7 +25,7 @@ If the hook is an HTTP URL then it will be invoked as HTTP POST. The request bod
If authentication succeed the HTTP response code must be 200 and the response body a valid SFTPGo user serialized as JSON. If the authentication fails the HTTP response code must be != 200 or the response body must be empty.
If the authentication succeeds, the user will be automatically added/updated inside the defined data provider. Actions defined for users added/updated will not be executed in this case.
If the authentication succeeds, the user will be automatically added/updated inside the defined data provider. Actions defined for users added/updated will not be executed in this case and an already logged in user with the same username will not be disconnected, you have to handle these things yourself.
The program hook must finish within 30 seconds, the HTTP hook timeout will use the global configuration for HTTP clients.

View file

@ -103,7 +103,7 @@ The configuration file contains the following sections:
- `exposed_headers`, list of strings.
- `allow_credentials` boolean.
- `max_age`, integer.
- `cache` struct containing cache configuration.
- `cache` struct containing cache configuration for the authenticated users.
- `enabled`, boolean, set to true to enable user caching. Default: true.
- `expiration_time`, integer. Expiration time, in minutes, for the cached users. 0 means unlimited. Default: 0.
- `max_size`, integer. Maximum number of users to cache. 0 means unlimited. Default: 50.

View file

@ -15,7 +15,19 @@ For system commands we have no direct control on file creation/deletion and so t
For these reasons we should limit system commands usage as much as possible, we currently support the following system commands:
- `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable support for Git repositories over SSH. They need to be installed and in your system's `PATH`.
- `rsync`. The `rsync` command needs to be installed and in your system's `PATH`. We cannot avoid that rsync creates symlinks, so if the user has the permission to create symlinks, we add the option `--safe-links` to the received rsync command if it is not already set. This should prevent creating symlinks that point outside the home dir. If the user cannot create symlinks, we add the option `--munge-links` if it is not already set. This should make symlinks unusable (but manually recoverable).
- `rsync`. The `rsync` command needs to be installed and in your system's `PATH`.
At least the following permissions are required to be able to run system commands:
- `list`
- `download`
- `upload`
- `create_dirs`
- `overwrite`
- `delete`
For `rsync` we cannot avoid that it creates symlinks so if the `create_symlinks` permission is granted we add the option `--safe-links`, if it is not already set, to the received `rsync` command. This should prevent to create symlinks that point outside the home directory.
If the user cannot create symlinks we add the option `--munge-links`, if it is not already set, to the received `rsync` command. This should make symlinks unusable (but manually recoverable).
SFTPGo support the following built-in SSH commands:

View file

@ -9,8 +9,8 @@ If you enable quota support a dataprovider query is required, to update the user
The caching configuration allows to set:
- `expiration_time` in minutes. If a user is cached for more than the specificied minutes it will be removed from the cache and a new dataprovider query will be performed. Please note that the `last_login` field will not be updated and `external_auth_hook`, `pre_login_hook` and `check_password_hook` will not be executed is the user is obtained from the cache.
- `max_size`. Maximum number of users to cache. When this limit is reached the user with the oldest `expiration_time` is removed from the cache. 0 means no limit however the cache size cannot exceed the number of users so if you have a small number of users you can leave this setting to 0.
- `expiration_time` in minutes. If a user is cached for more than the specificied minutes it will be removed from the cache and a new dataprovider query will be performed. Please note that the `last_login` field will not be updated and `external_auth_hook`, `pre_login_hook` and `check_password_hook` will not be executed if the user is obtained from the cache.
- `max_size`. Maximum number of users to cache. When this limit is reached the user with the oldest expiration date will be removed from the cache. 0 means no limit however the cache size cannot exceed the number of users so if you have a small number of users you can leave this setting to 0.
Users are automatically removed from the cache after an update/delete.

View file

@ -157,6 +157,8 @@ Output:
}
```
You can set the argument `--disconnect` to `1` to disconnect the user, if connected, after a successful update and so force it to login again and to use the new configuration. If this parameter is not specified the user will continue to use the old configuration as long as he is logged in.
## Get user by id
Command:

View file

@ -280,14 +280,15 @@ class SFTPGoApiRequests:
s3_key_prefix='', gcs_bucket='', gcs_key_prefix='', gcs_storage_class='', gcs_credentials_file='',
gcs_automatic_credentials='automatic', denied_login_methods=[], virtual_folders=[], denied_extensions=[],
allowed_extensions=[], s3_upload_part_size=0, s3_upload_concurrency=0, max_upload_file_size=0,
denied_protocols=[]):
denied_protocols=[], disconnect=0):
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,
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,
gcs_credentials_file, gcs_automatic_credentials, denied_login_methods, virtual_folders, denied_extensions,
allowed_extensions, s3_upload_part_size, s3_upload_concurrency, max_upload_file_size, denied_protocols)
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)), params={'disconnect':disconnect},
json=u, auth=self.auth, verify=self.verify)
self.printResponse(r)
def deleteUser(self, user_id):
@ -648,6 +649,11 @@ if __name__ == '__main__':
parserUpdateUser = subparsers.add_parser('update-user', help='Update an existing user')
parserUpdateUser.add_argument('id', type=int, help='User\'s ID to update')
parserUpdateUser.add_argument('--disconnect', type=int, choices=[0, 1], default=0,
help='0 means the user will not be disconnected and it will continue to use the old ' +
'configuration until connected. 1 means the user will be disconnected after a successful ' +
'update. It must login again and so it will be forced to use the new configuration. ' +
'Default: %(default)s')
addCommonUserArguments(parserUpdateUser)
parserDeleteUser = subparsers.add_parser('delete-user', help='Delete an existing user')
@ -708,9 +714,10 @@ if __name__ == '__main__':
parserLoadData.add_argument('-Q', '--scan-quota', type=int, choices=[0, 1, 2], default=0,
help='0 means no quota scan after a user is added/updated. 1 means always scan quota. 2 ' +
'means scan quota if the user has quota restrictions. Default: %(default)s')
parserLoadData.add_argument('-M', '--mode', type=int, choices=[0, 1], default=0,
parserLoadData.add_argument('-M', '--mode', type=int, choices=[0, 1, 2], default=0,
help='0 means new users are added, existing users are updated. 1 means new users are added,' +
' existing users are not modified. Default: %(default)s')
' existing users are not modified. 2 is the same as 0 but if an updated user is connected ' +
'it will be disconnected and so forced to use the new configuration Default: %(default)s')
parserUpdateQuotaUsage = subparsers.add_parser('update-quota-usage', help='Update the user used quota limits')
parserUpdateQuotaUsage.add_argument('username', type=str)
@ -772,7 +779,7 @@ if __name__ == '__main__':
args.s3_key_prefix, args.gcs_bucket, args.gcs_key_prefix, args.gcs_storage_class,
args.gcs_credentials_file, args.gcs_automatic_credentials, args.denied_login_methods,
args.virtual_folders, args.denied_extensions, args.allowed_extensions, args.s3_upload_part_size,
args.s3_upload_concurrency, args.max_upload_file_size, args.denied_protocols)
args.s3_upload_concurrency, args.max_upload_file_size, args.denied_protocols, args.disconnect)
elif args.command == 'delete-user':
api.deleteUser(args.id)
elif args.command == 'get-users':

View file

@ -456,14 +456,12 @@ func TestMaxSessions(t *testing.T) {
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
defer func() {
err := client.Quit()
assert.NoError(t, err)
}()
err = checkBasicFTP(client)
assert.NoError(t, err)
_, err = getFTPClient(user, false)
assert.Error(t, err)
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -685,7 +683,7 @@ func TestDeniedLoginMethod(t *testing.T) {
_, err = getFTPClient(user, false)
assert.Error(t, err)
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndPassword}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -708,7 +706,7 @@ func TestDeniedProtocols(t *testing.T) {
_, err = getFTPClient(user, false)
assert.Error(t, err)
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolWebDAV}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -756,7 +754,7 @@ func TestQuotaLimits(t *testing.T) {
// test quota size
user.QuotaSize = testFileSize - 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, true)
if assert.NoError(t, err) {
@ -770,7 +768,7 @@ func TestQuotaLimits(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -951,17 +949,14 @@ func TestRename(t *testing.T) {
assert.NoError(t, err)
}
user.Permissions[path.Join("/", testDir)] = []string{dataprovider.PermListItems}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
defer func() {
err := client.Quit()
assert.NoError(t, err)
}()
err = client.Rename(path.Join(testDir, testFileName), testFileName)
assert.Error(t, err)
err := client.Quit()
assert.NoError(t, err)
}
err = os.Remove(testFilePath)
@ -1028,11 +1023,6 @@ func TestStat(t *testing.T) {
assert.NoError(t, err)
client, err := getFTPClient(user, false)
if assert.NoError(t, err) {
defer func() {
err := client.Quit()
assert.NoError(t, err)
}()
subDir := "subdir"
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
@ -1051,6 +1041,8 @@ func TestStat(t *testing.T) {
assert.Error(t, err)
_, err = client.FileSize("missing file")
assert.Error(t, err)
err = client.Quit()
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
@ -1141,7 +1133,7 @@ func TestAllocate(t *testing.T) {
assert.NoError(t, err)
}
user.QuotaSize = 100
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1184,7 +1176,7 @@ func TestAllocate(t *testing.T) {
user.Filters.MaxUploadFileSize = 100
user.QuotaSize = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1217,11 +1209,6 @@ func TestChtimes(t *testing.T) {
assert.NoError(t, err)
client, err := getFTPClient(user, false)
if assert.NoError(t, err) {
defer func() {
err := client.Quit()
assert.NoError(t, err)
}()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
@ -1236,6 +1223,8 @@ func TestChtimes(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, ftp.StatusFile, code)
assert.Equal(t, fmt.Sprintf("Modify=%v; %v", mtime, testFileName), response)
err = client.Quit()
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
@ -1255,11 +1244,6 @@ func TestChmod(t *testing.T) {
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
defer func() {
err := client.Quit()
assert.NoError(t, err)
}()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(131072)
err = createTestFile(testFilePath, testFileSize)
@ -1278,6 +1262,8 @@ func TestChmod(t *testing.T) {
if assert.NoError(t, err) {
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
}
err = client.Quit()
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)

View file

@ -175,6 +175,9 @@ func restoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
err = dataprovider.UpdateUser(user)
user.Password = "[redacted]"
logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err)
if mode == 2 && err == nil {
disconnectUser(user.Username)
}
} else {
err = dataprovider.AddUser(user)
user.Password = "[redacted]"

View file

@ -2,12 +2,14 @@ package httpd
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/utils"
)
@ -100,6 +102,15 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
disconnect := 0
if _, ok := r.URL.Query()["disconnect"]; ok {
disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect"))
if err != nil {
err = fmt.Errorf("invalid disconnect parameter: %v", err)
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
}
user, err := dataprovider.GetUserByID(userID)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
@ -136,6 +147,9 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
} else {
sendAPIResponse(w, r, err, "User updated", http.StatusOK)
if disconnect == 1 {
disconnectUser(user.Username)
}
}
}
@ -156,5 +170,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
disconnectUser(user.Username)
}
}
func disconnectUser(username string) {
for _, stat := range common.Connections.GetStats() {
if stat.Username == username {
common.Connections.Close(stat.ConnectionID)
}
}
}

View file

@ -119,12 +119,15 @@ func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User,
}
// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode.
func UpdateUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {
func UpdateUser(user dataprovider.User, expectedStatusCode int, disconnect string) (dataprovider.User, []byte, error) {
var newUser dataprovider.User
var body []byte
url, err := addDisconnectQueryParam(buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)), disconnect)
if err != nil {
return user, body, err
}
userAsJSON, _ := json.Marshal(user)
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(userPath, strconv.FormatInt(user.ID, 10)),
bytes.NewBuffer(userAsJSON), "application/json")
resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "application/json")
if err != nil {
return user, body, err
}
@ -839,3 +842,16 @@ func addModeQueryParam(rawurl, mode string) (*url.URL, error) {
url.RawQuery = q.Encode()
return url, err
}
func addDisconnectQueryParam(rawurl, disconnect string) (*url.URL, error) {
url, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
q := url.Query()
if len(disconnect) > 0 {
q.Add("disconnect", disconnect)
}
url.RawQuery = q.Encode()
return url, err
}

View file

@ -92,6 +92,30 @@ var (
providerDriverName string
)
type fakeConnection struct {
*common.BaseConnection
command string
}
func (c *fakeConnection) Disconnect() error {
common.Connections.Remove(c.GetID())
return nil
}
func (c *fakeConnection) GetClientVersion() string {
return ""
}
func (c *fakeConnection) GetCommand() string {
return c.command
}
func (c *fakeConnection) GetRemoteAddress() string {
return ""
}
func (c *fakeConnection) SetConnDeadline() {}
func TestMain(m *testing.M) {
homeBasePath = os.TempDir()
logfilePath := filepath.Join(configDir, "sftpgo_api_test.log")
@ -221,7 +245,7 @@ func TestBasicUserHandling(t *testing.T) {
user.UploadBandwidth = 128
user.DownloadBandwidth = 64
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now())
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
assert.NoError(t, err)
@ -239,10 +263,10 @@ func TestUserStatus(t *testing.T) {
user, _, err := httpd.AddUser(u, http.StatusOK)
assert.NoError(t, err)
user.Status = 2
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
user.Status = 1
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -602,10 +626,10 @@ func TestUserPublicKey(t *testing.T) {
user, _, err := httpd.AddUser(u, http.StatusOK)
assert.NoError(t, err)
user.PublicKeys = []string{validPubKey, invalidPubKey}
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
user.PublicKeys = []string{validPubKey, validPubKey, validPubKey}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -656,10 +680,16 @@ func TestUpdateUser(t *testing.T) {
QuotaSize: 123,
QuotaFiles: 2,
})
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "invalid")
assert.NoError(t, err)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "0")
assert.NoError(t, err)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "1")
assert.NoError(t, err)
user.Permissions["/subdir"] = []string{}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
assert.Len(t, user.Permissions["/subdir"], 0)
assert.Len(t, user.VirtualFolders, 2)
@ -722,7 +752,7 @@ func TestUpdateUserQuotaUsage(t *testing.T) {
assert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, usedQuotaSize, user.UsedQuotaSize)
user.QuotaFiles = 100
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.UpdateQuotaUsage(u, "add", http.StatusOK)
assert.NoError(t, err)
@ -814,7 +844,7 @@ func TestUserFolderMapping(t *testing.T) {
QuotaSize: 0,
QuotaFiles: 0,
})
user2, _, err = httpd.UpdateUser(user2, http.StatusOK)
user2, _, err = httpd.UpdateUser(user2, http.StatusOK, "")
assert.NoError(t, err)
folders, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK)
assert.NoError(t, err)
@ -839,7 +869,7 @@ func TestUserFolderMapping(t *testing.T) {
},
VirtualPath: "/vdir1",
})
user2, _, err = httpd.UpdateUser(user2, http.StatusOK)
user2, _, err = httpd.UpdateUser(user2, http.StatusOK, "")
assert.NoError(t, err)
folders, _, err = httpd.GetFolders(0, 0, mappedPath2, http.StatusOK)
assert.NoError(t, err)
@ -900,7 +930,7 @@ func TestUserS3Config(t *testing.T) {
user.FsConfig.S3Config.AccessSecret = "Server-Access-Secret"
user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000"
user.FsConfig.S3Config.UploadPartSize = 8
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -917,7 +947,7 @@ func TestUserS3Config(t *testing.T) {
user.FsConfig.S3Config.Endpoint = "http://localhost:9000"
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir" //nolint:goconst
user.FsConfig.S3Config.UploadConcurrency = 5
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.FsConfig.Provider = 0
user.FsConfig.S3Config.Bucket = ""
@ -928,7 +958,7 @@ func TestUserS3Config(t *testing.T) {
user.FsConfig.S3Config.KeyPrefix = ""
user.FsConfig.S3Config.UploadPartSize = 0
user.FsConfig.S3Config.UploadConcurrency = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
// test user without access key and access secret (shared config state)
user.FsConfig.Provider = 1
@ -940,7 +970,7 @@ func TestUserS3Config(t *testing.T) {
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
user.FsConfig.S3Config.UploadPartSize = 6
user.FsConfig.S3Config.UploadConcurrency = 4
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -956,7 +986,7 @@ func TestUserGCSConfig(t *testing.T) {
user.FsConfig.Provider = 2
user.FsConfig.GCSConfig.Bucket = "test"
user.FsConfig.GCSConfig.Credentials = base64.StdEncoding.EncodeToString([]byte("fake credentials"))
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -971,7 +1001,7 @@ func TestUserGCSConfig(t *testing.T) {
assert.NoError(t, err)
user.FsConfig.GCSConfig.Credentials = ""
user.FsConfig.GCSConfig.AutomaticCredentials = 1
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.FsConfig.Provider = 1
user.FsConfig.S3Config.Bucket = "test1"
@ -980,12 +1010,12 @@ func TestUserGCSConfig(t *testing.T) {
user.FsConfig.S3Config.AccessSecret = "secret"
user.FsConfig.S3Config.Endpoint = "http://localhost:9000"
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.FsConfig.Provider = 2
user.FsConfig.GCSConfig.Bucket = "test1"
user.FsConfig.GCSConfig.Credentials = base64.StdEncoding.EncodeToString([]byte("fake credentials"))
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
@ -999,7 +1029,7 @@ func TestUpdateUserNoCredentials(t *testing.T) {
user.PublicKeys = []string{}
// password and public key will be omitted from json serialization if empty and so they will remain unchanged
// and no validation error will be raised
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -1009,7 +1039,7 @@ func TestUpdateUserEmptyHomeDir(t *testing.T) {
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
assert.NoError(t, err)
user.HomeDir = ""
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -1019,14 +1049,14 @@ func TestUpdateUserInvalidHomeDir(t *testing.T) {
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
assert.NoError(t, err)
user.HomeDir = "relative_path"
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestUpdateNonExistentUser(t *testing.T) {
_, _, err := httpd.UpdateUser(getTestUser(), http.StatusNotFound)
_, _, err := httpd.UpdateUser(getTestUser(), http.StatusNotFound, "")
assert.NoError(t, err)
}
@ -1182,6 +1212,43 @@ func TestGetConnections(t *testing.T) {
func TestCloseActiveConnection(t *testing.T) {
_, err := httpd.CloseConnection("non_existent_id", http.StatusNotFound)
assert.NoError(t, err)
user := getTestUser()
c := common.NewBaseConnection("connID", common.ProtocolSFTP, user, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
common.Connections.Add(fakeConn)
_, err = httpd.CloseConnection(c.GetID(), http.StatusOK)
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
}
func TestCloseConnectionAfterUserUpdateDelete(t *testing.T) {
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
assert.NoError(t, err)
c := common.NewBaseConnection("connID", common.ProtocolFTP, user, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
common.Connections.Add(fakeConn)
c1 := common.NewBaseConnection("connID1", common.ProtocolSFTP, user, nil)
fakeConn1 := &fakeConnection{
BaseConnection: c1,
}
common.Connections.Add(fakeConn1)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "0")
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 2)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "1")
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
common.Connections.Add(fakeConn)
common.Connections.Add(fakeConn1)
assert.Len(t, common.Connections.GetStats(), 2)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
}
func TestUserBaseDir(t *testing.T) {
@ -1264,7 +1331,7 @@ func TestProviderErrors(t *testing.T) {
assert.NoError(t, err)
_, _, err = httpd.GetUsers(1, 0, defaultUsername, http.StatusInternalServerError)
assert.NoError(t, err)
_, _, err = httpd.UpdateUser(dataprovider.User{}, http.StatusInternalServerError)
_, _, err = httpd.UpdateUser(dataprovider.User{}, http.StatusInternalServerError, "")
assert.NoError(t, err)
_, err = httpd.RemoveUser(dataprovider.User{}, http.StatusInternalServerError)
assert.NoError(t, err)
@ -1508,15 +1575,31 @@ func TestLoaddataMode(t *testing.T) {
user = users[0]
oldUploadBandwidth := user.UploadBandwidth
user.UploadBandwidth = oldUploadBandwidth + 128
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, _, err = httpd.Loaddata(backupFilePath, "0", "1", http.StatusOK)
assert.NoError(t, err)
c := common.NewBaseConnection("connID", common.ProtocolFTP, user, nil)
fakeConn := &fakeConnection{
BaseConnection: c,
}
common.Connections.Add(fakeConn)
assert.Len(t, common.Connections.GetStats(), 1)
users, _, err = httpd.GetUsers(1, 0, user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, len(users))
user = users[0]
assert.NotEqual(t, oldUploadBandwidth, user.UploadBandwidth)
_, _, err = httpd.Loaddata(backupFilePath, "0", "2", http.StatusOK)
assert.NoError(t, err)
// mode 2 will update the user and close the previous connection
assert.Len(t, common.Connections.GetStats(), 0)
users, _, err = httpd.GetUsers(1, 0, user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, len(users))
user = users[0]
assert.Equal(t, oldUploadBandwidth, user.UploadBandwidth)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.Remove(backupFilePath)
@ -2430,6 +2513,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("ssh_login_methods", dataprovider.SSHLoginMethodKeyboardInteractive)
form.Set("denied_protocols", common.ProtocolFTP)
form.Set("max_upload_file_size", "100")
form.Set("disconnect", "1")
b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b)
req.Header.Set("Content-Type", contentType)

View file

@ -374,7 +374,7 @@ func TestApiCallsWithBadURL(t *testing.T) {
MappedPath: os.TempDir(),
}
u := dataprovider.User{}
_, _, err := UpdateUser(u, http.StatusBadRequest)
_, _, err := UpdateUser(u, http.StatusBadRequest, "")
assert.Error(t, err)
_, err = RemoveUser(u, http.StatusNotFound)
assert.Error(t, err)
@ -405,7 +405,7 @@ func TestApiCallToNotListeningServer(t *testing.T) {
u := dataprovider.User{}
_, _, err := AddUser(u, http.StatusBadRequest)
assert.Error(t, err)
_, _, err = UpdateUser(u, http.StatusNotFound)
_, _, err = UpdateUser(u, http.StatusNotFound, "")
assert.Error(t, err)
_, err = RemoveUser(u, http.StatusNotFound)
assert.Error(t, err)

View file

@ -2,7 +2,7 @@ openapi: 3.0.1
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 1.9.5
version: 1.9.6
servers:
- url: /api/v1
@ -1124,6 +1124,17 @@ paths:
schema:
type: integer
format: int32
- in: query
name: disconnect
schema:
type: integer
enum:
- 0
- 1
description: >
Disconnect:
* `0` The user will not be disconnected and it will continue to use the old configuration until connected. This is the default
* `1` The user will be disconnected after a successful update. It must login again and so it will be forced to use the new configuration
requestBody:
required: true
content:
@ -1376,10 +1387,12 @@ paths:
enum:
- 0
- 1
- 2
description: >
Mode:
* `0` New users are added, existing users are updated. This is the default
* `1` New users are added, existing users are not modified
* `2` New users are added, existing users are updated and, if connected, they are disconnected and so forced to use the new configuration
responses:
200:
description: successful operation

View file

@ -602,6 +602,9 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
}
err = dataprovider.UpdateUser(updatedUser)
if err == nil {
if len(r.Form.Get("disconnect")) > 0 {
disconnectUser(user.Username)
}
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else {
renderUpdateUserPage(w, user, err.Error())

View file

@ -965,7 +965,7 @@ func TestLogin(t *testing.T) {
// testPubKey1 is not authorized
user.PublicKeys = []string{testPubKey1}
user.Password = ""
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, true)
if !assert.Error(t, err, "login with invalid public key must fail") {
@ -974,7 +974,7 @@ func TestLogin(t *testing.T) {
// login a user with multiple public keys, only the second one is valid
user.PublicKeys = []string{testPubKey1, testPubKey}
user.Password = ""
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, true)
if assert.NoError(t, err) {
@ -1201,7 +1201,7 @@ func TestLoginUserStatus(t *testing.T) {
assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin)
}
user.Status = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if !assert.Error(t, err, "login for a disabled user must fail") {
@ -1227,14 +1227,14 @@ func TestLoginUserExpiration(t *testing.T) {
assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin)
}
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) - 120000
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if !assert.Error(t, err, "login for an expired user must fail") {
client.Close()
}
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) + 120000
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -1284,7 +1284,7 @@ func TestDeniedProtocols(t *testing.T) {
client.Close()
}
user.Filters.DeniedProtocols = []string{common.ProtocolFTP, common.ProtocolWebDAV}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, true)
if assert.NoError(t, err) {
@ -1308,7 +1308,7 @@ func TestDeniedLoginMethods(t *testing.T) {
client.Close()
}
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.LoginMethodPassword}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, true)
if assert.NoError(t, err) {
@ -1316,7 +1316,7 @@ func TestDeniedLoginMethods(t *testing.T) {
assert.NoError(t, checkBasicSFTP(client))
}
user.Password = defaultPassword
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, false)
@ -1324,7 +1324,7 @@ func TestDeniedLoginMethods(t *testing.T) {
client.Close()
}
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPublicKey}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, false)
if assert.NoError(t, err) {
@ -1353,7 +1353,7 @@ func TestLoginWithIPFilters(t *testing.T) {
assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin)
}
user.Filters.AllowedIP = []string{"127.0.0.0/8"}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -1361,7 +1361,7 @@ func TestLoginWithIPFilters(t *testing.T) {
assert.NoError(t, checkBasicSFTP(client))
}
user.Filters.AllowedIP = []string{"172.19.0.0/16"}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if !assert.Error(t, err, "login from an not allowed IP must fail") {
@ -1380,7 +1380,7 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
user.Password = ""
user.PublicKeys = []string{}
// password and public key should remain unchanged
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -1400,7 +1400,7 @@ func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
user.Password = ""
user.PublicKeys = []string{}
// password and public key should remain unchanged
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -1427,14 +1427,14 @@ func TestLoginKeyboardInteractiveAuth(t *testing.T) {
assert.NoError(t, checkBasicSFTP(client))
}
user.Status = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getKeyboardInteractiveSftpClient(user, []string{"1", "2"})
if !assert.Error(t, err, "keyboard interactive auth must fail the user is disabled") {
client.Close()
}
user.Status = 1
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, -1), os.ModePerm)
assert.NoError(t, err)
@ -2113,7 +2113,7 @@ func TestQuotaFileReplace(t *testing.T) {
}
// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded
user.QuotaSize = testFileSize*2 - 1
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -2302,7 +2302,7 @@ func TestQuotaLimits(t *testing.T) {
// test quota size
user.QuotaSize = testFileSize - 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -2317,7 +2317,7 @@ func TestQuotaLimits(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -2461,7 +2461,7 @@ func TestExtensionsFilters(t *testing.T) {
DeniedExtensions: []string{},
},
}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -5862,13 +5862,13 @@ func TestSSHFileHash(t *testing.T) {
assert.NoError(t, err)
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermUpload}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = runSSHCommand("sha512sum "+testFileName, user, usePubKey)
assert.Error(t, err, "hash command with no list permission must fail")
user.Permissions["/"] = []string{dataprovider.PermAny}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
initialHash, err := computeHashForFile(sha512.New(), testFilePath)
@ -6293,7 +6293,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) {
user.QuotaSize = testFileSize * 10
user.VirtualFolders[1].QuotaSize = testFileSize
user.VirtualFolders[1].QuotaFiles = 10
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
assert.Equal(t, 1, user.QuotaFiles)
assert.Equal(t, testFileSize*10, user.QuotaSize)
@ -6443,7 +6443,7 @@ func TestSSHRemove(t *testing.T) {
// test remove dir with no delete perm
user.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -6491,7 +6491,7 @@ func TestBasicGitCommands(t *testing.T) {
assert.NoError(t, err, "unexpected error, out: %v", string(out))
user.QuotaFiles = 100000
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
out, err = pushToGitRepo(clonePath)
@ -6505,7 +6505,7 @@ func TestBasicGitCommands(t *testing.T) {
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
assert.NoError(t, err)
user.QuotaSize = user.UsedQuotaSize + 1
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
out, err = pushToGitRepo(clonePath)
assert.Error(t, err, "git push must fail if quota is exceeded, out: %v", string(out))
@ -6807,7 +6807,7 @@ func TestSCPExtensionsFilter(t *testing.T) {
DeniedExtensions: []string{},
},
}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = scpDownload(localPath, remoteDownPath, false, false)
assert.Error(t, err, "scp download must fail")
@ -7194,7 +7194,7 @@ func TestSCPQuotaSize(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = scpUpload(testFilePath2, remoteUpPath+".quota", true, false)
assert.Error(t, err, "user is over quota scp upload must fail")
@ -7365,7 +7365,7 @@ func TestSCPErrors(t *testing.T) {
assert.NoError(t, err)
user.UploadBandwidth = 512
user.DownloadBandwidth = 512
_, _, err = httpd.UpdateUser(user, http.StatusOK)
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
cmd := getScpDownloadCommand(localPath, remoteDownPath, false, false)
go func() {

View file

@ -399,6 +399,17 @@
</div>
</div>
{{if not .IsAdd}}
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idDisconnect" name="disconnect" aria-describedby="disconnectHelpBlock">
<label for="idDisconnect" class="form-check-label">Disconnect the user after the update</label>
<small id="disconnectHelpBlock" class="form-text text-muted">
This way you force the user to login again, if connected, and so to use the new configuration
</small>
</div>
</div>
{{end}}
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Submit</button>

View file

@ -687,7 +687,7 @@ func TestBasicUsersCache(t *testing.T) {
assert.False(t, cachedUser.IsExpired())
}
// cache is invalidated after a user modification
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok)
@ -836,7 +836,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
assert.True(t, ok)
// now remove user1 after an update
user1, _, err = httpd.UpdateUser(user1, http.StatusOK)
user1, _, err = httpd.UpdateUser(user1, http.StatusOK, "")
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
assert.False(t, ok)

View file

@ -377,12 +377,12 @@ func TestPreLoginHook(t *testing.T) {
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)
assert.NoError(t, err)
// update the user to remove it from the cache
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
// update the user to remove it from the cache
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.Status = 0
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
@ -583,7 +583,7 @@ func TestDeniedLoginMethod(t *testing.T) {
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -603,7 +603,7 @@ func TestDeniedProtocols(t *testing.T) {
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -644,7 +644,7 @@ func TestQuotaLimits(t *testing.T) {
// test quota size
user.QuotaSize = testFileSize - 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = uploadFile(testFilePath, testFileName+".quota", testFileSize, client)
assert.Error(t, err)
@ -653,7 +653,7 @@ func TestQuotaLimits(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = uploadFile(testFilePath1, testFileName1, testFileSize1, client)
assert.Error(t, err)
@ -914,7 +914,7 @@ func TestStat(t *testing.T) {
err = uploadFile(testFilePath, path.Join("/", subDir, testFileName), testFileSize, client)
assert.NoError(t, err)
user.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermDownload}
user, _, err = httpd.UpdateUser(user, http.StatusOK)
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = client.Stat(testFileName)
assert.NoError(t, err)