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:
parent
dbed110d02
commit
3925c7ff95
20 changed files with 270 additions and 110 deletions
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue