From fd52475ae24d921620c92e75fa672c7ed20b62de Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 17 Jul 2022 18:48:41 +0200 Subject: [PATCH] shared mode: ensure to clear webdav cache for deleted users Signed-off-by: Nicola Murino --- dataprovider/bolt.go | 2 +- dataprovider/dataprovider.go | 8 ++++---- dataprovider/memory.go | 2 +- dataprovider/mysql.go | 4 ++-- dataprovider/pgsql.go | 4 ++-- dataprovider/scheduler.go | 14 ++++++++++++-- dataprovider/sqlcommon.go | 22 +++++++++++++++++++--- dataprovider/sqlite.go | 4 ++-- dataprovider/sqlqueries.go | 20 +++++++++++++------- dataprovider/user.go | 2 ++ httpd/webadmin.go | 9 +++++++-- sftpd/sftpd_test.go | 6 ++++-- 12 files changed, 69 insertions(+), 28 deletions(-) diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index d4835ec1..7a1cfa01 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -626,7 +626,7 @@ func (p *BoltProvider) updateUser(user *User) error { }) } -func (p *BoltProvider) deleteUser(user User) error { +func (p *BoltProvider) deleteUser(user User, softDelete bool) error { return p.dbHandle.Update(func(tx *bolt.Tx) error { bucket, err := p.getUsersBucket(tx) if err != nil { diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index fb6dd477..3a6363b6 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -632,7 +632,7 @@ type Provider interface { userExists(username string) (User, error) addUser(user *User) error updateUser(user *User) error - deleteUser(user User) error + deleteUser(user User, softDelete bool) error updateUserPassword(username, password string) error getUsers(limit int, offset int, order string) ([]User, error) dumpUsers() ([]User, error) @@ -1577,7 +1577,7 @@ func DeleteEventRule(name string, executor, ipAddress string) error { if err != nil { return err } - err = provider.deleteEventRule(rule, config.GetShared() == 1) + err = provider.deleteEventRule(rule, config.IsShared == 1) if err == nil { EventManager.RemoveRule(rule.Name) executeAction(operationDelete, executor, ipAddress, actionObjectEventRule, rule.Name, &rule) @@ -1716,10 +1716,10 @@ func DeleteUser(username, executor, ipAddress string) error { if err != nil { return err } - err = provider.deleteUser(user) + err = provider.deleteUser(user, config.IsShared == 1) if err == nil { RemoveCachedWebDAVUser(user.Username) - delayedQuotaUpdater.resetUserQuota(username) + delayedQuotaUpdater.resetUserQuota(user.Username) cachedPasswords.Remove(username) executeAction(operationDelete, executor, ipAddress, actionObjectUser, user.Username, &user) } diff --git a/dataprovider/memory.go b/dataprovider/memory.go index bb649a3b..106f6817 100644 --- a/dataprovider/memory.go +++ b/dataprovider/memory.go @@ -377,7 +377,7 @@ func (p *MemoryProvider) updateUser(user *User) error { return nil } -func (p *MemoryProvider) deleteUser(user User) error { +func (p *MemoryProvider) deleteUser(user User, softDelete bool) error { p.dbHandle.Lock() defer p.dbHandle.Unlock() if p.dbHandle.isClosed { diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index 5eaf16fd..cac48d25 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -292,8 +292,8 @@ func (p *MySQLProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p *MySQLProvider) deleteUser(user User) error { - return sqlCommonDeleteUser(user, p.dbHandle) +func (p *MySQLProvider) deleteUser(user User, softDelete bool) error { + return sqlCommonDeleteUser(user, softDelete, p.dbHandle) } func (p *MySQLProvider) updateUserPassword(username, password string) error { diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index a1d0f835..9eb03b51 100644 --- a/dataprovider/pgsql.go +++ b/dataprovider/pgsql.go @@ -267,8 +267,8 @@ func (p *PGSQLProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p *PGSQLProvider) deleteUser(user User) error { - return sqlCommonDeleteUser(user, p.dbHandle) +func (p *PGSQLProvider) deleteUser(user User, softDelete bool) error { + return sqlCommonDeleteUser(user, softDelete, p.dbHandle) } func (p *PGSQLProvider) updateUserPassword(username, password string) error { diff --git a/dataprovider/scheduler.go b/dataprovider/scheduler.go index 4578b365..ca7455d2 100644 --- a/dataprovider/scheduler.go +++ b/dataprovider/scheduler.go @@ -71,8 +71,18 @@ func checkCacheUpdates() { return } for _, user := range users { - providerLog(logger.LevelDebug, "invalidate caches for user %#v", user.Username) - webDAVUsersCache.swap(&user) + providerLog(logger.LevelDebug, "invalidate caches for user %q", user.Username) + if user.DeletedAt > 0 { + deletedAt := util.GetTimeFromMsecSinceEpoch(user.DeletedAt) + if deletedAt.Add(30 * time.Minute).Before(time.Now()) { + providerLog(logger.LevelDebug, "removing user %q deleted at %s", user.Username, deletedAt) + go provider.deleteUser(user, false) //nolint:errcheck + } + webDAVUsersCache.remove(user.Username) + delayedQuotaUpdater.resetUserQuota(user.Username) + } else { + webDAVUsersCache.swap(&user) + } cachedPasswords.Remove(user.Username) } diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index 39ba3638..b52f1106 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -989,11 +989,27 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error { }) } -func sqlCommonDeleteUser(user User, dbHandle *sql.DB) error { +func sqlCommonDeleteUser(user User, softDelete bool, dbHandle *sql.DB) error { ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) defer cancel() - q := getDeleteUserQuery() + q := getDeleteUserQuery(softDelete) + if softDelete { + return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error { + if err := sqlCommonClearUserFolderMapping(ctx, &user, tx); err != nil { + return err + } + if err := sqlCommonClearUserGroupMapping(ctx, &user, tx); err != nil { + return err + } + ts := util.GetTimeAsMsSinceEpoch(time.Now()) + res, err := tx.ExecContext(ctx, q, ts, ts, user.Username) + if err != nil { + return err + } + return sqlCommonRequireRowAffected(res) + }) + } res, err := dbHandle.ExecContext(ctx, q, user.ID) if err != nil { return err @@ -1703,7 +1719,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) { &user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate, &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, &additionalInfo, &description, &email, &user.CreatedAt, &user.UpdatedAt, &user.UploadDataTransfer, &user.DownloadDataTransfer, - &user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer) + &user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return user, util.NewRecordNotFoundError(err.Error()) diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index 19e3a1e1..1ec0b021 100644 --- a/dataprovider/sqlite.go +++ b/dataprovider/sqlite.go @@ -239,8 +239,8 @@ func (p *SQLiteProvider) updateUser(user *User) error { return sqlCommonUpdateUser(user, p.dbHandle) } -func (p *SQLiteProvider) deleteUser(user User) error { - return sqlCommonDeleteUser(user, p.dbHandle) +func (p *SQLiteProvider) deleteUser(user User, softDelete bool) error { + return sqlCommonDeleteUser(user, softDelete, p.dbHandle) } func (p *SQLiteProvider) updateUserPassword(username, password string) error { diff --git a/dataprovider/sqlqueries.go b/dataprovider/sqlqueries.go index 5974e659..b0ada083 100644 --- a/dataprovider/sqlqueries.go +++ b/dataprovider/sqlqueries.go @@ -12,7 +12,7 @@ const ( selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," + "used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem," + "additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer," + - "used_upload_data_transfer,used_download_data_transfer" + "used_upload_data_transfer,used_download_data_transfer,deleted_at" selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem" selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login" selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id" @@ -380,12 +380,13 @@ func getRelatedAdminsForAPIKeysQuery(apiKeys []APIKey) string { } func getUserByUsernameQuery() string { - return fmt.Sprintf(`SELECT %s FROM %s WHERE username = %s`, selectUserFields, sqlTableUsers, sqlPlaceholders[0]) + return fmt.Sprintf(`SELECT %s FROM %s WHERE username = %s AND deleted_at = 0`, + selectUserFields, sqlTableUsers, sqlPlaceholders[0]) } func getUsersQuery(order string) string { - return fmt.Sprintf(`SELECT %s FROM %s ORDER BY username %s LIMIT %s OFFSET %s`, selectUserFields, sqlTableUsers, - order, sqlPlaceholders[0], sqlPlaceholders[1]) + return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0 ORDER BY username %s LIMIT %s OFFSET %s`, + selectUserFields, sqlTableUsers, order, sqlPlaceholders[0], sqlPlaceholders[1]) } func getUsersForQuotaCheckQuery(numArgs int) string { @@ -407,11 +408,12 @@ func getUsersForQuotaCheckQuery(numArgs int) string { } func getRecentlyUpdatedUsersQuery() string { - return fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s`, selectUserFields, sqlTableUsers, sqlPlaceholders[0]) + return fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s OR deleted_at > 0`, + selectUserFields, sqlTableUsers, sqlPlaceholders[0]) } func getDumpUsersQuery() string { - return fmt.Sprintf(`SELECT %s FROM %s`, selectUserFields, sqlTableUsers) + return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0`, selectUserFields, sqlTableUsers) } func getDumpFoldersQuery() string { @@ -493,7 +495,11 @@ func getUpdateUserPasswordQuery() string { return fmt.Sprintf(`UPDATE %s SET password=%s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1]) } -func getDeleteUserQuery() string { +func getDeleteUserQuery(softDelete bool) string { + if softDelete { + return fmt.Sprintf(`UPDATE %s SET updated_at=%s,deleted_at=%s WHERE username = %s`, + sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2]) + } return fmt.Sprintf(`DELETE FROM %s WHERE id = %s`, sqlTableUsers, sqlPlaceholders[0]) } diff --git a/dataprovider/user.go b/dataprovider/user.go index a636aa02..f0b117a7 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -128,6 +128,8 @@ type User struct { fsCache map[string]vfs.Fs `json:"-"` // true if group settings are already applied for this user groupSettingsApplied bool `json:"-"` + // in multi node setups we mark the user as deleted to be able to update the webdav cache + DeletedAt int64 `json:"-"` } // GetFilesystem returns the base filesystem for this user diff --git a/httpd/webadmin.go b/httpd/webadmin.go index 90fd0a00..58810165 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -790,8 +790,13 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use title = "User template" currentURL = webTemplateUser } - if user.Password != "" && user.IsPasswordHashed() && mode == userPageModeUpdate { - user.Password = redactedSecret + if user.Password != "" && user.IsPasswordHashed() { + switch mode { + case userPageModeUpdate: + user.Password = redactedSecret + default: + user.Password = "" + } } user.FsConfig.RedactedSecret = redactedSecret data := userPage{ diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 7c96d156..9e7ea0fb 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -1235,8 +1235,10 @@ func TestRealPath(t *testing.T) { assert.ErrorIs(t, err, os.ErrPermission) err = os.Remove(filepath.Join(localUser.GetHomeDir(), subdir, "temp")) assert.NoError(t, err) - err = os.RemoveAll(filepath.Join(localUser.GetHomeDir(), subdir)) - assert.NoError(t, err) + if user.Username == localUser.Username { + err = os.RemoveAll(filepath.Join(localUser.GetHomeDir(), subdir)) + assert.NoError(t, err) + } } } _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)