mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
shared mode: ensure to clear webdav cache for deleted users
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
55b47cf741
commit
fd52475ae2
12 changed files with 69 additions and 28 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue