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

@@ -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)