dataprovider: add timestamp fields for users and admins

This commit is contained in:
Nicola Murino 2021-08-19 15:51:43 +02:00
parent b99d4ce82e
commit be3857d572
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
52 changed files with 725 additions and 76 deletions

View file

@ -1,3 +1,4 @@
//go:build !noportable
// +build !noportable
package cmd

View file

@ -1,3 +1,4 @@
//go:build noportable
// +build noportable
package cmd

View file

@ -1,3 +1,4 @@
//go:build linux
// +build linux
package config

View file

@ -1,3 +1,4 @@
//go:build !linux
// +build !linux
package config

View file

@ -68,6 +68,12 @@ type Admin struct {
Filters AdminFilters `json:"filters,omitempty"`
Description string `json:"description,omitempty"`
AdditionalInfo string `json:"additional_info,omitempty"`
// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
CreatedAt int64 `json:"created_at"`
// last update time as unix timestamp in milliseconds
UpdatedAt int64 `json:"updated_at"`
// Last login as unix timestamp in milliseconds
LastLogin int64 `json:"last_login"`
}
func (a *Admin) checkPassword() error {
@ -260,6 +266,9 @@ func (a *Admin) getACopy() Admin {
Filters: filters,
AdditionalInfo: a.AdditionalInfo,
Description: a.Description,
LastLogin: a.LastLogin,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}

View file

@ -125,9 +125,6 @@ func (k *APIKey) validate() error {
if err := k.checkKey(); err != nil {
return err
}
if k.CreatedAt == 0 {
k.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
}
if k.User != "" && k.Admin != "" {
return util.NewValidationError("an API key can be related to a user or an admin, not both")
}

View file

@ -1,3 +1,4 @@
//go:build !nobolt
// +build !nobolt
package dataprovider
@ -19,7 +20,7 @@ import (
)
const (
boltDatabaseVersion = 11
boltDatabaseVersion = 12
)
var (
@ -191,6 +192,36 @@ func (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {
})
}
func (p *BoltProvider) setUpdatedAt(username string) {
p.dbHandle.Update(func(tx *bolt.Tx) error { //nolint:errcheck
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
var u []byte
if u = bucket.Get([]byte(username)); u == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to update updated at", username))
}
var user User
err = json.Unmarshal(u, &user)
if err != nil {
return err
}
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(user)
if err != nil {
return err
}
err = bucket.Put([]byte(username), buf)
if err == nil {
providerLog(logger.LevelDebug, "updated at set for user %#v", username)
} else {
providerLog(logger.LevelWarn, "error setting updated_at for user %#v: %v", username, err)
}
return err
})
}
func (p *BoltProvider) updateLastLogin(username string) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getUsersBucket(tx)
@ -221,6 +252,36 @@ func (p *BoltProvider) updateLastLogin(username string) error {
})
}
func (p *BoltProvider) updateAdminLastLogin(username string) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getAdminsBucket(tx)
if err != nil {
return err
}
var a []byte
if a = bucket.Get([]byte(username)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("admin %#v does not exist, unable to update last login", username))
}
var admin Admin
err = json.Unmarshal(a, &admin)
if err != nil {
return err
}
admin.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(admin)
if err != nil {
return err
}
err = bucket.Put([]byte(username), buf)
if err == nil {
providerLog(logger.LevelDebug, "last login updated for admin %#v", username)
return err
}
providerLog(logger.LevelWarn, "error updating last login for admin %#v: %v", username, err)
return err
})
}
func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getUsersBucket(tx)
@ -300,6 +361,9 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
return err
}
admin.ID = int64(id)
admin.LastLogin = 0
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(admin)
if err != nil {
return err
@ -330,6 +394,9 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
}
admin.ID = oldAdmin.ID
admin.CreatedAt = oldAdmin.CreatedAt
admin.LastLogin = oldAdmin.LastLogin
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(admin)
if err != nil {
return err
@ -478,6 +545,8 @@ func (p *BoltProvider) addUser(user *User) error {
user.UsedQuotaSize = 0
user.UsedQuotaFiles = 0
user.LastLogin = 0
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
for idx := range user.VirtualFolders {
err = addUserToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, folderBucket)
if err != nil {
@ -532,6 +601,8 @@ func (p *BoltProvider) updateUser(user *User) error {
user.UsedQuotaSize = oldUser.UsedQuotaSize
user.UsedQuotaFiles = oldUser.UsedQuotaFiles
user.LastLogin = oldUser.LastLogin
user.CreatedAt = oldUser.CreatedAt
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(user)
if err != nil {
return err
@ -916,7 +987,9 @@ func (p *BoltProvider) addAPIKey(apiKey *APIKey) error {
return err
}
apiKey.ID = int64(id)
apiKey.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
apiKey.LastUseAt = 0
buf, err := json.Marshal(apiKey)
if err != nil {
return err
@ -1077,7 +1150,9 @@ func (p *BoltProvider) migrateDatabase() error {
logger.ErrorToConsole("%v", err)
return err
case version == 10:
return updateBoltDatabaseVersion(p.dbHandle, 11)
return updateBoltDatabaseVersion(p.dbHandle, 12)
case version == 11:
return updateBoltDatabaseVersion(p.dbHandle, 12)
default:
if version > boltDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
@ -1099,6 +1174,8 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
return errors.New("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 12:
return updateBoltDatabaseVersion(p.dbHandle, 10)
case 11:
return updateBoltDatabaseVersion(p.dbHandle, 10)
default:

View file

@ -1,3 +1,4 @@
//go:build nobolt
// +build nobolt
package dataprovider

View file

@ -392,6 +392,8 @@ type Provider interface {
getUsers(limit int, offset int, order string) ([]User, error)
dumpUsers() ([]User, error)
updateLastLogin(username string) error
updateAdminLastLogin(username string) error
setUpdatedAt(username string)
getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error)
getFolderByName(name string) (vfs.BaseVirtualFolder, error)
addFolder(folder *vfs.BaseVirtualFolder) error
@ -813,7 +815,7 @@ func UpdateAPIKeyLastUse(apiKey *APIKey) error {
}
// UpdateLastLogin updates the last login field for the given SFTPGo user
func UpdateLastLogin(user *User) error {
func UpdateLastLogin(user *User) {
lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
diff := -time.Until(lastLogin)
if diff < 0 || diff > lastLoginMinDelay {
@ -821,9 +823,16 @@ func UpdateLastLogin(user *User) error {
if err == nil {
webDAVUsersCache.updateLastLogin(user.Username)
}
return err
}
return nil
}
// UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
func UpdateAdminLastLogin(admin *Admin) {
lastLogin := util.GetTimeFromMsecSinceEpoch(admin.LastLogin)
diff := -time.Until(lastLogin)
if diff < 0 || diff > lastLoginMinDelay {
provider.updateAdminLastLogin(admin.Username) //nolint:errcheck
}
}
// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd.
@ -1026,7 +1035,14 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string) error {
err := provider.updateFolder(folder)
if err == nil {
for _, user := range users {
RemoveCachedWebDAVUser(user)
provider.setUpdatedAt(user)
u, err := provider.userExists(user)
if err == nil {
webDAVUsersCache.swap(&u)
executeAction(operationUpdate, &u)
} else {
RemoveCachedWebDAVUser(user)
}
}
}
return err
@ -1041,6 +1057,7 @@ func DeleteFolder(folderName string) error {
err = provider.deleteFolder(&folder)
if err == nil {
for _, user := range folder.Users {
provider.setUpdatedAt(user)
RemoveCachedWebDAVUser(user)
}
delayedQuotaUpdater.resetFolderQuota(folderName)
@ -2252,6 +2269,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro
userUsedQuotaFiles := u.UsedQuotaFiles
userLastQuotaUpdate := u.LastQuotaUpdate
userLastLogin := u.LastLogin
userCreatedAt := u.CreatedAt
err = json.Unmarshal(out, &u)
if err != nil {
return u, fmt.Errorf("invalid pre-login hook response %#v, error: %v", string(out), err)
@ -2261,9 +2279,11 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro
u.UsedQuotaFiles = userUsedQuotaFiles
u.LastQuotaUpdate = userLastQuotaUpdate
u.LastLogin = userLastLogin
u.CreatedAt = userCreatedAt
if userID == 0 {
err = provider.addUser(&u)
} else {
u.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
err = provider.updateUser(&u)
if err == nil {
webDAVUsersCache.swap(&u)
@ -2464,6 +2484,8 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastQuotaUpdate = u.LastQuotaUpdate
user.LastLogin = u.LastLogin
user.CreatedAt = u.CreatedAt
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
err = provider.updateUser(&user)
if err == nil {
webDAVUsersCache.swap(&user)

View file

@ -158,6 +158,20 @@ func (p *MemoryProvider) updateAPIKeyLastUse(keyID string) error {
return nil
}
func (p *MemoryProvider) setUpdatedAt(username string) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return
}
user, err := p.userExistsInternal(username)
if err != nil {
return
}
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.users[user.Username] = user
}
func (p *MemoryProvider) updateLastLogin(username string) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
@ -173,6 +187,21 @@ func (p *MemoryProvider) updateLastLogin(username string) error {
return nil
}
func (p *MemoryProvider) updateAdminLastLogin(username string) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
admin, err := p.adminExistsInternal(username)
if err != nil {
return err
}
admin.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.admins[admin.Username] = admin
return nil
}
func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
@ -235,6 +264,8 @@ func (p *MemoryProvider) addUser(user *User) error {
user.UsedQuotaSize = 0
user.UsedQuotaFiles = 0
user.LastLogin = 0
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.VirtualFolders = p.joinVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
@ -268,6 +299,8 @@ func (p *MemoryProvider) updateUser(user *User) error {
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastLogin = u.LastLogin
user.CreatedAt = u.CreatedAt
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.ID = u.ID
// pre-login and external auth hook will use the passed *user so save a copy
p.dbHandle.users[user.Username] = user.getACopy()
@ -407,6 +440,9 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
return fmt.Errorf("admin %#v already exists", admin.Username)
}
admin.ID = p.getNextAdminID()
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.LastLogin = 0
p.dbHandle.admins[admin.Username] = admin.getACopy()
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
sort.Strings(p.dbHandle.adminsUsernames)
@ -428,6 +464,9 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
return err
}
admin.ID = a.ID
admin.CreatedAt = a.CreatedAt
admin.LastLogin = a.LastLogin
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.admins[admin.Username] = admin.getACopy()
return nil
}
@ -825,7 +864,9 @@ func (p *MemoryProvider) addAPIKey(apiKey *APIKey) error {
if err == nil {
return fmt.Errorf("API key %#v already exists", apiKey.KeyID)
}
apiKey.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
apiKey.LastUseAt = 0
p.dbHandle.apiKeys[apiKey.KeyID] = apiKey.getACopy()
p.dbHandle.apiKeysIDs = append(p.dbHandle.apiKeysIDs, apiKey.KeyID)
sort.Strings(p.dbHandle.apiKeysIDs)
@ -1041,23 +1082,52 @@ func (p *MemoryProvider) reloadConfig() error {
return err
}
if err := p.restoreAPIKeys(&dump); err != nil {
return err
}
providerLog(logger.LevelDebug, "config loaded from file: %#v", p.dbHandle.configFile)
return nil
}
func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {
for _, apiKey := range dump.APIKeys {
if apiKey.KeyID == "" {
return fmt.Errorf("cannot restore an empty API key: %+v", apiKey)
}
k, err := p.apiKeyExists(apiKey.KeyID)
apiKey := apiKey // pin
if err == nil {
apiKey.ID = k.ID
err = UpdateAPIKey(&apiKey)
if err != nil {
providerLog(logger.LevelWarn, "error updating API key %#v: %v", apiKey.KeyID, err)
return err
}
} else {
err = AddAPIKey(&apiKey)
if err != nil {
providerLog(logger.LevelWarn, "error adding API key %#v: %v", apiKey.KeyID, err)
return err
}
}
}
return nil
}
func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
for _, admin := range dump.Admins {
a, err := p.adminExists(admin.Username)
admin := admin // pin
if err == nil {
admin.ID = a.ID
err = p.updateAdmin(&admin)
err = UpdateAdmin(&admin)
if err != nil {
providerLog(logger.LevelWarn, "error updating admin %#v: %v", admin.Username, err)
return err
}
} else {
err = p.addAdmin(&admin)
err = AddAdmin(&admin)
if err != nil {
providerLog(logger.LevelWarn, "error adding admin %#v: %v", admin.Username, err)
return err
@ -1073,14 +1143,14 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
f, err := p.getFolderByName(folder.Name)
if err == nil {
folder.ID = f.ID
err = p.updateFolder(&folder)
err = UpdateFolder(&folder, f.Users)
if err != nil {
providerLog(logger.LevelWarn, "error updating folder %#v: %v", folder.Name, err)
return err
}
} else {
folder.Users = nil
err = p.addFolder(&folder)
err = AddFolder(&folder)
if err != nil {
providerLog(logger.LevelWarn, "error adding folder %#v: %v", folder.Name, err)
return err
@ -1096,13 +1166,13 @@ func (p *MemoryProvider) restoreUsers(dump *BackupData) error {
u, err := p.userExists(user.Username)
if err == nil {
user.ID = u.ID
err = p.updateUser(&user)
err = UpdateUser(&user)
if err != nil {
providerLog(logger.LevelWarn, "error updating user %#v: %v", user.Username, err)
return err
}
} else {
err = p.addUser(&user)
err = AddUser(&user)
if err != nil {
providerLog(logger.LevelWarn, "error adding user %#v: %v", user.Username, err)
return err

View file

@ -1,3 +1,4 @@
//go:build !nomysql
// +build !nomysql
package dataprovider
@ -46,6 +47,22 @@ const (
"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_admin_id_fk_admins_id` FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
mysqlV11DownSQL = "DROP TABLE `{{api_keys}}` CASCADE;"
mysqlV12SQL = "ALTER TABLE `{{admins}}` ADD COLUMN `created_at` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{admins}}` ALTER COLUMN `created_at` DROP DEFAULT;" +
"ALTER TABLE `{{admins}}` ADD COLUMN `updated_at` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{admins}}` ALTER COLUMN `updated_at` DROP DEFAULT;" +
"ALTER TABLE `{{admins}}` ADD COLUMN `last_login` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{admins}}` ALTER COLUMN `last_login` DROP DEFAULT;" +
"ALTER TABLE `{{users}}` ADD COLUMN `created_at` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{users}}` ALTER COLUMN `created_at` DROP DEFAULT;" +
"ALTER TABLE `{{users}}` ADD COLUMN `updated_at` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{users}}` ALTER COLUMN `updated_at` DROP DEFAULT;" +
"CREATE INDEX `{{prefix}}users_updated_at_idx` ON `{{users}}` (`updated_at`);"
mysqlV12DownSQL = "ALTER TABLE `{{admins}}` DROP COLUMN `updated_at`;" +
"ALTER TABLE `{{admins}}` DROP COLUMN `created_at`;" +
"ALTER TABLE `{{admins}}` DROP COLUMN `last_login`;" +
"ALTER TABLE `{{users}}` DROP COLUMN `created_at`;" +
"ALTER TABLE `{{users}}` DROP COLUMN `updated_at`;"
)
// MySQLProvider auth provider for MySQL/MariaDB database
@ -117,10 +134,18 @@ func (p *MySQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *MySQLProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
func (p *MySQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *MySQLProvider) updateAdminLastLogin(username string) error {
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
}
func (p *MySQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
@ -276,6 +301,8 @@ func (p *MySQLProvider) migrateDatabase() error {
return err
case version == 10:
return updateMySQLDatabaseFromV10(p.dbHandle)
case version == 11:
return updateMySQLDatabaseFromV11(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
@ -298,6 +325,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 12:
return downgradeMySQLDatabaseFromV12(p.dbHandle)
case 11:
return downgradeMySQLDatabaseFromV11(p.dbHandle)
default:
@ -306,13 +335,45 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
}
func updateMySQLDatabaseFromV10(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom10To11(dbHandle)
if err := updateMySQLDatabaseFrom10To11(dbHandle); err != nil {
return err
}
return updateMySQLDatabaseFromV11(dbHandle)
}
func updateMySQLDatabaseFromV11(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom11To12(dbHandle)
}
func downgradeMySQLDatabaseFromV12(dbHandle *sql.DB) error {
if err := downgradeMySQLDatabaseFrom12To11(dbHandle); err != nil {
return err
}
return downgradeMySQLDatabaseFromV11(dbHandle)
}
func downgradeMySQLDatabaseFromV11(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFrom11To10(dbHandle)
}
func updateMySQLDatabaseFrom11To12(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 11 -> 12")
providerLog(logger.LevelInfo, "updating database version: 11 -> 12")
sql := strings.ReplaceAll(mysqlV12SQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 12)
}
func downgradeMySQLDatabaseFrom12To11(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 12 -> 11")
providerLog(logger.LevelInfo, "downgrading database version: 12 -> 11")
sql := strings.ReplaceAll(mysqlV12DownSQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 11)
}
func updateMySQLDatabaseFrom10To11(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 10 -> 11")
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")

View file

@ -1,3 +1,4 @@
//go:build nomysql
// +build nomysql
package dataprovider

View file

@ -1,3 +1,4 @@
//go:build !nopgsql
// +build !nopgsql
package dataprovider
@ -57,6 +58,24 @@ CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
`
pgsqlV11DownSQL = `DROP TABLE "{{api_keys}}" CASCADE;`
pgsqlV12SQL = `ALTER TABLE "{{admins}}" ADD COLUMN "created_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{admins}}" ALTER COLUMN "created_at" DROP DEFAULT;
ALTER TABLE "{{admins}}" ADD COLUMN "updated_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{admins}}" ALTER COLUMN "updated_at" DROP DEFAULT;
ALTER TABLE "{{admins}}" ADD COLUMN "last_login" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{admins}}" ALTER COLUMN "last_login" DROP DEFAULT;
ALTER TABLE "{{users}}" ADD COLUMN "created_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{users}}" ALTER COLUMN "created_at" DROP DEFAULT;
ALTER TABLE "{{users}}" ADD COLUMN "updated_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{users}}" ALTER COLUMN "updated_at" DROP DEFAULT;
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
`
pgsqlV12DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "updated_at" CASCADE;
ALTER TABLE "{{users}}" DROP COLUMN "created_at" CASCADE;
ALTER TABLE "{{admins}}" DROP COLUMN "created_at" CASCADE;
ALTER TABLE "{{admins}}" DROP COLUMN "updated_at" CASCADE;
ALTER TABLE "{{admins}}" DROP COLUMN "last_login" CASCADE;
`
)
// PGSQLProvider auth provider for PostgreSQL database
@ -128,10 +147,18 @@ func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *PGSQLProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
func (p *PGSQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *PGSQLProvider) updateAdminLastLogin(username string) error {
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
}
func (p *PGSQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
@ -293,6 +320,8 @@ func (p *PGSQLProvider) migrateDatabase() error {
return err
case version == 10:
return updatePGSQLDatabaseFromV10(p.dbHandle)
case version == 11:
return updatePGSQLDatabaseFromV11(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
@ -315,6 +344,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 12:
return downgradePGSQLDatabaseFromV12(p.dbHandle)
case 11:
return downgradePGSQLDatabaseFromV11(p.dbHandle)
default:
@ -323,13 +354,45 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
}
func updatePGSQLDatabaseFromV10(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom10To11(dbHandle)
if err := updatePGSQLDatabaseFrom10To11(dbHandle); err != nil {
return err
}
return updatePGSQLDatabaseFromV11(dbHandle)
}
func updatePGSQLDatabaseFromV11(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom11To12(dbHandle)
}
func downgradePGSQLDatabaseFromV12(dbHandle *sql.DB) error {
if err := downgradePGSQLDatabaseFrom12To11(dbHandle); err != nil {
return err
}
return downgradePGSQLDatabaseFromV11(dbHandle)
}
func downgradePGSQLDatabaseFromV11(dbHandle *sql.DB) error {
return downgradePGSQLDatabaseFrom11To10(dbHandle)
}
func updatePGSQLDatabaseFrom11To12(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 11 -> 12")
providerLog(logger.LevelInfo, "updating database version: 11 -> 12")
sql := strings.ReplaceAll(pgsqlV12SQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 12)
}
func downgradePGSQLDatabaseFrom12To11(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 12 -> 11")
providerLog(logger.LevelInfo, "downgrading database version: 12 -> 11")
sql := strings.ReplaceAll(pgsqlV12DownSQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 11)
}
func updatePGSQLDatabaseFrom10To11(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 10 -> 11")
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")

View file

@ -1,3 +1,4 @@
//go:build nopgsql
// +build nopgsql
package dataprovider

View file

@ -19,7 +19,7 @@ import (
)
const (
sqlDatabaseVersion = 11
sqlDatabaseVersion = 12
defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second
)
@ -75,7 +75,7 @@ func sqlCommonAddAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, apiKey.KeyID, apiKey.Name, apiKey.Key, apiKey.Scope, apiKey.CreatedAt,
_, err = stmt.ExecContext(ctx, apiKey.KeyID, apiKey.Name, apiKey.Key, apiKey.Scope, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()), apiKey.LastUseAt, apiKey.ExpiresAt, apiKey.Description,
userID, adminID)
return err
@ -251,7 +251,8 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
}
_, err = stmt.ExecContext(ctx, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
string(filters), admin.AdditionalInfo, admin.Description)
string(filters), admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()))
return err
}
@ -282,7 +283,7 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
}
_, err = stmt.ExecContext(ctx, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
admin.AdditionalInfo, admin.Description, admin.Username)
admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Username)
return err
}
@ -486,6 +487,43 @@ func sqlCommonUpdateAPIKeyLastUse(keyID string, dbHandle *sql.DB) error {
return err
}
func sqlCommonUpdateAdminLastLogin(username string, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateAdminLastLoginQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return err
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, util.GetTimeAsMsSinceEpoch(time.Now()), username)
if err == nil {
providerLog(logger.LevelDebug, "last login updated for admin %#v", username)
} else {
providerLog(logger.LevelWarn, "error updating last login for admin %#v: %v", username, err)
}
return err
}
func sqlCommonSetUpdatedAt(username string, dbHandle *sql.DB) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getSetUpdateAtQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, util.GetTimeAsMsSinceEpoch(time.Now()), username)
if err == nil {
providerLog(logger.LevelDebug, "updated_at set for user %#v", username)
} else {
providerLog(logger.LevelWarn, "error setting updated_at for user %#v: %v", username, err)
}
}
func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
@ -539,7 +577,8 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
}
_, err = stmt.ExecContext(ctx, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters),
string(fsConfig), user.AdditionalInfo, user.Description)
string(fsConfig), user.AdditionalInfo, user.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()))
if err != nil {
return err
}
@ -581,7 +620,7 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
}
_, err = stmt.ExecContext(ctx, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status, user.ExpirationDate,
string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.ID)
string(filters), string(fsConfig), user.AdditionalInfo, user.Description, util.GetTimeAsMsSinceEpoch(time.Now()), user.ID)
if err != nil {
return err
}
@ -702,7 +741,7 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
var email, filters, additionalInfo, permissions, description sql.NullString
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
&filters, &additionalInfo, &description)
&filters, &additionalInfo, &description, &admin.CreatedAt, &admin.UpdatedAt, &admin.LastLogin)
if err != nil {
if err == sql.ErrNoRows {
@ -752,7 +791,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
&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)
&additionalInfo, &description, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return user, util.NewRecordNotFoundError(err.Error())

View file

@ -1,3 +1,4 @@
//go:build !nosqlite
// +build !nosqlite
package dataprovider
@ -52,6 +53,20 @@ CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "api_keys" ("admin_id");
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "api_keys" ("user_id");
`
sqliteV11DownSQL = `DROP TABLE "{{api_keys}}";`
sqliteV12SQL = `ALTER TABLE "{{admins}}" ADD COLUMN "created_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{admins}}" ADD COLUMN "updated_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{admins}}" ADD COLUMN "last_login" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{users}}" ADD COLUMN "created_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{users}}" ADD COLUMN "updated_at" bigint DEFAULT 0 NOT NULL;
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
`
sqliteV12DownSQL = `DROP INDEX "{{prefix}}users_updated_at_idx";
ALTER TABLE "{{users}}" DROP COLUMN "updated_at";
ALTER TABLE "{{users}}" DROP COLUMN "created_at";
ALTER TABLE "{{admins}}" DROP COLUMN "created_at";
ALTER TABLE "{{admins}}" DROP COLUMN "updated_at";
ALTER TABLE "{{admins}}" DROP COLUMN "last_login";
`
)
// SQLiteProvider auth provider for SQLite database
@ -115,10 +130,18 @@ func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *SQLiteProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
func (p *SQLiteProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p *SQLiteProvider) updateAdminLastLogin(username string) error {
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
}
func (p *SQLiteProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
@ -274,6 +297,8 @@ func (p *SQLiteProvider) migrateDatabase() error {
return err
case version == 10:
return updateSQLiteDatabaseFromV10(p.dbHandle)
case version == 11:
return updateSQLiteDatabaseFromV11(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
@ -296,6 +321,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 12:
return downgradeSQLiteDatabaseFromV12(p.dbHandle)
case 11:
return downgradeSQLiteDatabaseFromV11(p.dbHandle)
default:
@ -304,13 +331,45 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
}
func updateSQLiteDatabaseFromV10(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom10To11(dbHandle)
if err := updateSQLiteDatabaseFrom10To11(dbHandle); err != nil {
return err
}
return updateSQLiteDatabaseFromV11(dbHandle)
}
func updateSQLiteDatabaseFromV11(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom11To12(dbHandle)
}
func downgradeSQLiteDatabaseFromV12(dbHandle *sql.DB) error {
if err := downgradeSQLiteDatabaseFrom12To11(dbHandle); err != nil {
return err
}
return downgradeSQLiteDatabaseFromV11(dbHandle)
}
func downgradeSQLiteDatabaseFromV11(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFrom11To10(dbHandle)
}
func updateSQLiteDatabaseFrom11To12(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 11 -> 12")
providerLog(logger.LevelInfo, "updating database version: 11 -> 12")
sql := strings.ReplaceAll(sqliteV12SQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 12)
}
func downgradeSQLiteDatabaseFrom12To11(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 12 -> 11")
providerLog(logger.LevelInfo, "downgrading database version: 12 -> 11")
sql := strings.ReplaceAll(sqliteV12DownSQL, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 11)
}
func updateSQLiteDatabaseFrom10To11(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 10 -> 11")
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")

View file

@ -1,3 +1,4 @@
//go:build nosqlite
// +build nosqlite
package dataprovider

View file

@ -11,9 +11,9 @@ import (
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"
"additional_info,description,created_at,updated_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"
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"
)
@ -43,15 +43,16 @@ func getDumpAdminsQuery() string {
}
func getAddAdminQuery() string {
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info,description)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
sqlPlaceholders[8], sqlPlaceholders[9])
}
func getUpdateAdminQuery() string {
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v,description=%v
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v,description=%v,updated_at=%v
WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8])
}
func getDeleteAdminQuery() string {
@ -156,10 +157,18 @@ func getUpdateQuotaQuery(reset bool) string {
WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getSetUpdateAtQuery() string {
return fmt.Sprintf(`UPDATE %v SET updated_at = %v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getUpdateLastLoginQuery() string {
return fmt.Sprintf(`UPDATE %v SET last_login = %v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getUpdateAdminLastLoginQuery() string {
return fmt.Sprintf(`UPDATE %v SET last_login = %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getUpdateAPIKeyLastUseQuery() string {
return fmt.Sprintf(`UPDATE %v SET last_use_at = %v WHERE key_id = %v`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1])
}
@ -172,20 +181,20 @@ func getQuotaQuery() string {
func getAddUserQuery() string {
return fmt.Sprintf(`INSERT INTO %v (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,status,last_login,expiration_date,filters,
filesystem,additional_info,description)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
filesystem,additional_info,description,created_at,updated_at)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v,%v,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13],
sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17])
sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19])
}
func getUpdateUserQuery() string {
return fmt.Sprintf(`UPDATE %v SET password=%v,public_keys=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v,
quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v,status=%v,expiration_date=%v,filters=%v,filesystem=%v,
additional_info=%v,description=%v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
additional_info=%v,description=%v,updated_at=%v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15],
sqlPlaceholders[16], sqlPlaceholders[17])
sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18])
}
func getDeleteUserQuery() string {

View file

@ -1037,6 +1037,8 @@ func (u *User) getACopy() User {
Filters: filters,
AdditionalInfo: u.AdditionalInfo,
Description: u.Description,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
},
VirtualFolders: virtualFolders,
FsConfig: u.FsConfig.GetACopy(),

View file

@ -20,12 +20,12 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
Starting a SFTPGo instance is simple:
```shell
docker run --name some-sftpgo -p 127.0.0.1:8080:8080 -p 2022:2022 -d "drakkan/sftpgo:tag"
docker run --name some-sftpgo -p 8080:8080 -p 2022:2022 -d "drakkan/sftpgo:tag"
```
... where `some-sftpgo` is the name you want to assign to your container, and `tag` is the tag specifying the SFTPGo version you want. See the list above for relevant tags.
Now visit [http://localhost:8080/web/admin](http://localhost:8080/web/admin), create the first admin and then log in and create a new SFTPGo user. The SFTP service is available on port 2022.
Now visit [http://localhost:8080/web/admin](http://localhost:8080/web/admin), replacing `localhost` with the appropriate IP address if SFTPGo is not reachable on localhost, create the first admin and a new SFTPGo user. The SFTP service is available on port 2022.
If you don't want to persist any files, for example for testing purposes, you can run an SFTPGo instance like this:
@ -102,7 +102,7 @@ The Docker documentation is a good starting point for understanding the differen
```shell
docker run --name some-sftpgo \
-p 127.0.0.1:8080:8090 \
-p 8080:8090 \
-p 2022:2022 \
--mount type=bind,source=/my/own/sftpgodata,target=/srv/sftpgo \
--mount type=bind,source=/my/own/sftpgohome,target=/var/lib/sftpgo \
@ -150,7 +150,7 @@ With the above directory permissions, you can start a SFTPGo instance like this:
```shell
docker run --name some-sftpgo \
--user 1100:1100 \
-p 127.0.0.1:8080:8080 \
-p 8080:8080 \
-p 2022:2022 \
--mount type=bind,source="${PWD}/data",target=/srv/sftpgo \
--mount type=bind,source="${PWD}/config",target=/var/lib/sftpgo \

View file

@ -203,7 +203,7 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
}
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP, username: %#v, home_dir: %#v remote addr: %#v",
user.ID, user.Username, user.HomeDir, ipAddr)
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
return connection, nil
}
@ -249,7 +249,7 @@ func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsCo
}
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP using a TLS certificate, username: %#v, home_dir: %#v remote addr: %#v",
dbUser.ID, dbUser.Username, dbUser.HomeDir, ipAddr)
dataprovider.UpdateLastLogin(&dbUser) //nolint:errcheck
dataprovider.UpdateLastLogin(&dbUser)
return connection, nil
}
}

10
go.mod
View file

@ -7,7 +7,7 @@ require (
github.com/Azure/azure-storage-blob-go v0.14.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
github.com/aws/aws-sdk-go v1.40.24
github.com/aws/aws-sdk-go v1.40.25
github.com/cockroachdb/cockroach-go/v2 v2.1.1
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
github.com/fatih/color v1.12.0 // indirect
@ -61,17 +61,17 @@ require (
gocloud.dev v0.23.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2
golang.org/x/sys v0.0.0-20210819072135-bce67f096156
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
google.golang.org/api v0.54.0
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d // indirect
google.golang.org/genproto v0.0.0-20210818220304-27ea9cc85d9f // indirect
google.golang.org/grpc v1.40.0
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require (
cloud.google.com/go v0.92.3 // indirect
cloud.google.com/go v0.93.3 // indirect
cloud.google.com/go/kms v0.1.0 // indirect
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
@ -82,7 +82,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/fsnotify/fsnotify v1.5.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/goccy/go-json v0.7.6 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect

19
go.sum
View file

@ -26,8 +26,8 @@ cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSU
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.92.3 h1:VWuKmJ8pyOrb7doM0NnQDYngKv+zTicI8BaMsnIA9gA=
cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.93.3 h1:wPBktZFzYBcCZVARvwVKqH1uEj+aLXofJEtrb4oOsio=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -124,8 +124,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.40.24 h1:qtXDYFzAxEmmZaa+4JA9loBqOujO0vm4ZOJoEmjG21E=
github.com/aws/aws-sdk-go v1.40.24/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.40.25 h1:Depnx7O86HWgOCLD5nMto6F9Ju85Q1QuFDnbpZYQWno=
github.com/aws/aws-sdk-go v1.40.25/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM=
github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
@ -216,8 +216,9 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.0 h1:NO5hkcB+srp1x6QmwvNZLeaOgbM8cmBTN32THzjvu2k=
github.com/fsnotify/fsnotify v1.5.0/go.mod h1:BX0DCEr5pT4jm2CnQdVP1lFV521fcCNcyEeNp4DQQDk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
@ -915,8 +916,8 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo=
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819072135-bce67f096156 h1:f7XLk/QXGE6IM4HjJ4ttFFlPSwJ65A1apfDd+mmViR0=
golang.org/x/sys v0.0.0-20210819072135-bce67f096156/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1112,8 +1113,8 @@ google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d h1:fPtHPeysWvGVJwQFKu3B7H2DB2sOEsW7UTayKkWESKw=
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210818220304-27ea9cc85d9f h1:enWPderunHptc5pzJkSYGx0olpF8goXzG0rY3kL0eSg=
google.golang.org/genproto v0.0.0-20210818220304-27ea9cc85d9f/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

View file

@ -395,6 +395,79 @@ func TestBasicUserHandling(t *testing.T) {
assert.NoError(t, err)
}
func TestUserTimestamps(t *testing.T) {
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err, string(resp))
createdAt := user.CreatedAt
updatedAt := user.UpdatedAt
assert.Equal(t, int64(0), user.LastLogin)
assert.Greater(t, createdAt, int64(0))
assert.Greater(t, updatedAt, int64(0))
mappedPath := filepath.Join(os.TempDir(), "mapped_dir")
folderName := filepath.Base(mappedPath)
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderName,
MappedPath: mappedPath,
},
VirtualPath: "/vdir",
})
time.Sleep(10 * time.Millisecond)
user, resp, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err, string(resp))
assert.Equal(t, int64(0), user.LastLogin)
assert.Equal(t, createdAt, user.CreatedAt)
assert.Greater(t, user.UpdatedAt, updatedAt)
updatedAt = user.UpdatedAt
// after a folder update or delete the user updated_at field should change
folder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, folder.Users, 1)
time.Sleep(10 * time.Millisecond)
_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, int64(0), user.LastLogin)
assert.Equal(t, createdAt, user.CreatedAt)
assert.Greater(t, user.UpdatedAt, updatedAt)
updatedAt = user.UpdatedAt
time.Sleep(10 * time.Millisecond)
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, int64(0), user.LastLogin)
assert.Equal(t, createdAt, user.CreatedAt)
assert.Greater(t, user.UpdatedAt, updatedAt)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestAdminTimestamps(t *testing.T) {
admin := getTestAdmin()
admin.Username = altAdminUsername
admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
assert.NoError(t, err)
createdAt := admin.CreatedAt
updatedAt := admin.UpdatedAt
assert.Equal(t, int64(0), admin.LastLogin)
assert.Greater(t, createdAt, int64(0))
assert.Greater(t, updatedAt, int64(0))
time.Sleep(10 * time.Millisecond)
admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, int64(0), admin.LastLogin)
assert.Equal(t, createdAt, admin.CreatedAt)
assert.Greater(t, admin.UpdatedAt, updatedAt)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
}
func TestHTTPUserAuthentication(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -757,6 +830,26 @@ func TestAdminInvalidCredentials(t *testing.T) {
assert.Equal(t, dataprovider.ErrInvalidCredentials.Error(), responseHolder["error"].(string))
}
func TestAdminLastLogin(t *testing.T) {
a := getTestAdmin()
a.Username = altAdminUsername
a.Password = altAdminPassword
admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, int64(0), admin.LastLogin)
_, _, err = httpdtest.GetToken(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)
assert.NoError(t, err)
assert.Greater(t, admin.LastLogin, int64(0))
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
}
func TestAdminAllowList(t *testing.T) {
a := getTestAdmin()
a.Username = altAdminUsername
@ -1361,7 +1454,7 @@ func TestUpdateUserEmptyPassword(t *testing.T) {
assert.NoError(t, err)
userNoPwd, _, err := httpdtest.UpdateUserWithJSON(user, http.StatusOK, "", asJSON)
assert.NoError(t, err)
assert.Equal(t, user, userNoPwd) // the password is hidden so the user must be equal
assert.Equal(t, user.Password, userNoPwd.Password) // the password is hidden
// check the password within the data provider
dbUser, err = dataprovider.UserExists(u.Username)
assert.NoError(t, err)
@ -1705,6 +1798,7 @@ func TestUserS3Config(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
user.VirtualFolders = nil
secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Access-Secret", "", "")
user.FsConfig.S3Config.AccessSecret = secret
@ -1749,6 +1843,7 @@ func TestUserS3Config(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
// shared credential test for add instead of update
user, _, err = httpdtest.AddUser(user, http.StatusCreated)
assert.NoError(t, err)
@ -1795,6 +1890,7 @@ func TestUserGCSConfig(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake credentials", "", "")
_, _, err = httpdtest.AddUser(user, http.StatusCreated)
assert.Error(t, err)
@ -1861,6 +1957,7 @@ func TestUserAzureBlobConfig(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Account-Key", "", "")
user.FsConfig.AzBlobConfig.AccountKey = secret
_, _, err = httpdtest.AddUser(user, http.StatusCreated)
@ -1901,6 +1998,7 @@ func TestUserAzureBlobConfig(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
// sas test for add instead of update
user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
AzBlobFsConfig: sdk.AzBlobFsConfig{
@ -1956,6 +2054,7 @@ func TestUserCryptFs(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
secret := kms.NewSecret(kms.SecretStatusSecretBox, "invalid encrypted payload", "", "")
user.FsConfig.CryptConfig.Passphrase = secret
_, _, err = httpdtest.AddUser(user, http.StatusCreated)
@ -2036,6 +2135,7 @@ func TestUserSFTPFs(t *testing.T) {
assert.NoError(t, err)
user.Password = defaultPassword
user.ID = 0
user.CreatedAt = 0
secret := kms.NewSecret(kms.SecretStatusSecretBox, "invalid encrypted payload", "", "")
user.FsConfig.SFTPConfig.Password = secret
_, _, err = httpdtest.AddUser(user, http.StatusCreated)
@ -4120,6 +4220,69 @@ func TestUpdateAdminMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
}
func TestAdminLastLoginWithAPIKey(t *testing.T) {
admin := getTestAdmin()
admin.Username = altAdminUsername
admin.Filters.AllowAPIKeyAuth = true
admin, resp, err := httpdtest.AddAdmin(admin, http.StatusCreated)
assert.NoError(t, err, string(resp))
assert.Equal(t, int64(0), admin.LastLogin)
apiKey := dataprovider.APIKey{
Name: "admin API key",
Scope: dataprovider.APIKeyScopeAdmin,
Admin: altAdminUsername,
}
apiKey, resp, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
assert.NoError(t, err, string(resp))
req, err := http.NewRequest(http.MethodGet, versionPath, nil)
assert.NoError(t, err)
setAPIKeyForReq(req, apiKey.Key, admin.Username)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)
assert.NoError(t, err)
assert.Greater(t, admin.LastLogin, int64(0))
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
}
func TestUserLastLoginWithAPIKey(t *testing.T) {
user := getTestUser()
user.Filters.AllowAPIKeyAuth = true
user, resp, err := httpdtest.AddUser(user, http.StatusCreated)
assert.NoError(t, err, string(resp))
assert.Equal(t, int64(0), user.LastLogin)
apiKey := dataprovider.APIKey{
Name: "user API key",
Scope: dataprovider.APIKeyScopeUser,
User: user.Username,
}
apiKey, resp, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)
assert.NoError(t, err, string(resp))
req, err := http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err)
setAPIKeyForReq(req, apiKey.Key, "")
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Greater(t, user.LastLogin, int64(0))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestAdminHandlingWithAPIKeys(t *testing.T) {
sysAdmin, _, err := httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)
assert.NoError(t, err)
@ -4260,6 +4423,8 @@ func TestUserHandlingWithAPIKey(t *testing.T) {
setAPIKeyForReq(req, apiKey.Key, "")
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
@ -4506,6 +4671,7 @@ func TestUpdateUserInvalidParamsMock(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr)
userID := user.ID
user.ID = 0
user.CreatedAt = 0
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)

View file

@ -341,7 +341,7 @@ func authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTA
return err
}
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
dataprovider.UpdateAdminLastLogin(&admin)
return nil
}
@ -397,7 +397,7 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
return err
}
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
updateLoginMetrics(&user, ipAddr, nil)
return nil

View file

@ -2969,6 +2969,14 @@ components:
type: integer
format: int32
description: 'Maximum download bandwidth as KB/s, 0 means unlimited'
created_at:
type: integer
format: int64
description: 'creation time as unix timestamp in milliseconds. It will be 0 for users created before v2.2.0'
updated_at:
type: integer
format: int64
description: last update time as unix timestamp in milliseconds
last_login:
type: integer
format: int64
@ -3032,6 +3040,18 @@ components:
additional_info:
type: string
description: Free form text field
created_at:
type: integer
format: int64
description: 'creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0'
updated_at:
type: integer
format: int64
description: last update time as unix timestamp in milliseconds
last_login:
type: integer
format: int64
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
APIKey:
type: object
properties:

View file

@ -197,7 +197,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
return
}
updateLoginMetrics(&user, ipAddr, err)
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
http.Redirect(w, r, webClientFilesPath, http.StatusFound)
}
@ -307,6 +307,7 @@ func (s *httpdServer) loginAdmin(w http.ResponseWriter, r *http.Request, admin *
}
http.Redirect(w, r, webUsersPath, http.StatusFound)
dataprovider.UpdateAdminLastLogin(admin)
}
func (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {
@ -377,7 +378,7 @@ func (s *httpdServer) generateAndSendUserToken(w http.ResponseWriter, r *http.Re
return
}
updateLoginMetrics(&user, ipAddr, err)
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
render.JSON(w, r, resp)
}
@ -413,6 +414,7 @@ func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Reques
return
}
dataprovider.UpdateAdminLastLogin(&admin)
render.JSON(w, r, resp)
}

View file

@ -1058,6 +1058,11 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
return errors.New("admin ID mismatch")
}
}
if expected.CreatedAt > 0 {
if expected.CreatedAt != actual.CreatedAt {
return fmt.Errorf("created_at mismatch %v != %v", expected.CreatedAt, actual.CreatedAt)
}
}
if err := compareAdminEqualFields(expected, actual); err != nil {
return err
}
@ -1116,6 +1121,11 @@ func checkUser(expected *dataprovider.User, actual *dataprovider.User) error {
return errors.New("user ID mismatch")
}
}
if expected.CreatedAt > 0 {
if expected.CreatedAt != actual.CreatedAt {
return fmt.Errorf("created_at mismatch %v != %v", expected.CreatedAt, actual.CreatedAt)
}
}
if len(expected.Permissions) != len(actual.Permissions) {
return errors.New("permissions mismatch")
}

View file

@ -1,3 +1,4 @@
//go:build linux
// +build linux
package logger

View file

@ -1,3 +1,4 @@
//go:build !linux
// +build !linux
package logger

View file

@ -1,3 +1,4 @@
//go:build !nometrics
// +build !nometrics
// Package metrics provides Prometheus metrics support

View file

@ -1,3 +1,4 @@
//go:build nometrics
// +build nometrics
package metric

View file

@ -166,6 +166,10 @@ type BaseUser struct {
DownloadBandwidth int64 `json:"download_bandwidth"`
// Last login as unix timestamp in milliseconds
LastLogin int64 `json:"last_login"`
// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
CreatedAt int64 `json:"created_at"`
// last update time as unix timestamp in milliseconds
UpdatedAt int64 `json:"updated_at"`
// Additional restrictions
Filters UserFilters `json:"filters"`
// optional description, for example full name

View file

@ -286,15 +286,7 @@ func (s *Service) loadInitialData() error {
}
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
err := httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode)
if err != nil {
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
if err != nil {
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan)
err := httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan)
if err != nil {
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
}
@ -302,5 +294,13 @@ func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
if err != nil {
return fmt.Errorf("unable to restore users from file %#v: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
if err != nil {
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode)
if err != nil {
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
}
return nil
}

View file

@ -1,3 +1,4 @@
//go:build !noportable
// +build !noportable
package service

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package service

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package sftpd

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package sftpd

View file

@ -406,7 +406,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
logger.Log(logger.LevelDebug, common.ProtocolSSH, connectionID,
"User %#v, logged in with: %#v, from ip: %#v, client version %#v",
user.Username, loginType, ipAddr, string(sconn.ClientVersion()))
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
sshConnection := common.NewSSHConnection(connectionID, conn)
common.Connections.AddSSHConnection(sshConnection)

View file

@ -43,7 +43,7 @@ func ServeSubSystemConnection(user *dataprovider.User, connectionID string, read
logger.Warn(logSender, connectionID, "unable to check fs root: %v close fs error: %v", err, errClose)
return err
}
dataprovider.UpdateLastLogin(user) //nolint:errcheck
dataprovider.UpdateLastLogin(user)
connection := &Connection{
BaseConnection: common.NewBaseConnection(connectionID, common.ProtocolSFTP, "", "", *user),

View file

@ -1,3 +1,4 @@
//go:build !noazblob
// +build !noazblob
package vfs

View file

@ -1,3 +1,4 @@
//go:build noazblob
// +build noazblob
package vfs

View file

@ -1,3 +1,4 @@
//go:build !nogcs
// +build !nogcs
package vfs

View file

@ -1,3 +1,4 @@
//go:build nogcs
// +build nogcs
package vfs

View file

@ -1,3 +1,4 @@
//go:build !nos3
// +build !nos3
package vfs

View file

@ -1,3 +1,4 @@
//go:build nos3
// +build nos3
package vfs

View file

@ -1,3 +1,4 @@
//go:build !darwin && !linux && !freebsd
// +build !darwin,!linux,!freebsd
package vfs

View file

@ -1,3 +1,4 @@
//go:build linux
// +build linux
package vfs

View file

@ -1,3 +1,4 @@
//go:build freebsd || darwin
// +build freebsd darwin
package vfs

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package vfs

View file

@ -1118,10 +1118,22 @@ func TestCachedUserWithFolders(t *testing.T) {
folder, err := dataprovider.GetFolderByName(folderName)
assert.NoError(t, err)
// updating a used folder should invalidate the cache
// updating a used folder should invalidate the cache only if the fs changed
err = dataprovider.UpdateFolder(&folder, folder.Users)
assert.NoError(t, err)
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.True(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)
cachedUser, ok = dataprovider.GetCachedWebDAVUser(username)
if assert.True(t, ok) {
assert.False(t, cachedUser.IsExpired())
}
// changing the folder path should invalidate the cache
folder.MappedPath = filepath.Join(os.TempDir(), "anotherpath")
err = dataprovider.UpdateFolder(&folder, folder.Users)
assert.NoError(t, err)
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)

View file

@ -208,7 +208,7 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
dataprovider.UpdateLastLogin(&user)
if s.checkRequestMethod(ctx, r, connection) {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")