user: add a free text field

Fixes #230
This commit is contained in:
Nicola Murino 2020-11-25 22:26:34 +01:00
parent 2054dfd83d
commit 4bb9d07dde
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
15 changed files with 128 additions and 26 deletions

View file

@ -38,6 +38,7 @@ const (
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `unique_mapping` UNIQUE (`user_id`, `folder_id`);" +
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_folder_id_fk_folders_id` FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
)
// MySQLProvider auth provider for MySQL/MariaDB database
@ -217,6 +218,8 @@ func (p MySQLProvider) migrateDatabase() error {
return updateMySQLDatabaseFromV3(p.dbHandle)
case 4:
return updateMySQLDatabaseFromV4(p.dbHandle)
case 5:
return updateMySQLDatabaseFromV5(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
@ -247,7 +250,15 @@ func updateMySQLDatabaseFromV3(dbHandle *sql.DB) error {
}
func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom4To5(dbHandle)
err := updateMySQLDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV5(dbHandle)
}
func updateMySQLDatabaseFromV5(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom5To6(dbHandle)
}
func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -271,3 +282,10 @@ func updateMySQLDatabaseFrom3To4(dbHandle *sql.DB) error {
func updateMySQLDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(mysqlV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}

View file

@ -37,6 +37,7 @@ ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "folders_mapping_user_id_fk_use
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
`
pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
)
// PGSQLProvider auth provider for PostgreSQL database
@ -216,6 +217,8 @@ func (p PGSQLProvider) migrateDatabase() error {
return updatePGSQLDatabaseFromV3(p.dbHandle)
case 4:
return updatePGSQLDatabaseFromV4(p.dbHandle)
case 5:
return updatePGSQLDatabaseFromV5(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
@ -246,7 +249,15 @@ func updatePGSQLDatabaseFromV3(dbHandle *sql.DB) error {
}
func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom4To5(dbHandle)
err := updatePGSQLDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV5(dbHandle)
}
func updatePGSQLDatabaseFromV5(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom5To6(dbHandle)
}
func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -270,3 +281,10 @@ func updatePGSQLDatabaseFrom3To4(dbHandle *sql.DB) error {
func updatePGSQLDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(pgsqlV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}

View file

@ -14,7 +14,7 @@ import (
)
const (
sqlDatabaseVersion = 5
sqlDatabaseVersion = 6
initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);"
defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second
@ -218,7 +218,7 @@ 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))
string(fsConfig), user.AdditionalInfo)
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
@ -272,7 +272,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.ID)
string(filters), string(fsConfig), user.AdditionalInfo, user.ID)
if err != nil {
sqlCommonRollbackTransaction(tx)
return err
@ -391,15 +391,18 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
var publicKey sql.NullString
var filters sql.NullString
var fsConfig sql.NullString
var additionalInfo sql.NullString
var err error
if row != nil {
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)
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo)
} else {
err = rows.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)
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo)
}
if err != nil {
if err == sql.ErrNoRows {
@ -440,6 +443,9 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
user.FsConfig = fs
}
}
if additionalInfo.Valid {
user.AdditionalInfo = additionalInfo.String
}
return user, err
}

View file

@ -63,6 +63,7 @@ ALTER TABLE "new__users" RENAME TO "{{users}}";
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
`
sqliteV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
)
// SQLiteProvider auth provider for SQLite database
@ -239,6 +240,8 @@ func (p SQLiteProvider) migrateDatabase() error {
return updateSQLiteDatabaseFromV3(p.dbHandle)
case 4:
return updateSQLiteDatabaseFromV4(p.dbHandle)
case 5:
return updateSQLiteDatabaseFromV5(p.dbHandle)
default:
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
}
@ -269,7 +272,15 @@ func updateSQLiteDatabaseFromV3(dbHandle *sql.DB) error {
}
func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom4To5(dbHandle)
err := updateSQLiteDatabaseFrom4To5(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV5(dbHandle)
}
func updateSQLiteDatabaseFromV5(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom5To6(dbHandle)
}
func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -293,3 +304,10 @@ func updateSQLiteDatabaseFrom3To4(dbHandle *sql.DB) error {
func updateSQLiteDatabaseFrom4To5(dbHandle *sql.DB) error {
return sqlCommonUpdateDatabaseFrom4To5(dbHandle)
}
func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 5 -> 6")
providerLog(logger.LevelInfo, "updating database version: 5 -> 6")
sql := strings.Replace(sqliteV6SQL, "{{users}}", sqlTableUsers, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}

View file

@ -10,7 +10,7 @@ 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"
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem,additional_info"
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update"
)
@ -72,19 +72,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)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
filesystem,additional_info)
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%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[14], sqlPlaceholders[15], sqlPlaceholders[16])
}
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
WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v,status=%v,expiration_date=%v,filters=%v,filesystem=%v,
additional_info=%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[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15],
sqlPlaceholders[16])
}
func getDeleteUserQuery() string {

View file

@ -214,6 +214,8 @@ type User struct {
Filters UserFilters `json:"filters"`
// Filesystem configuration details
FsConfig Filesystem `json:"filesystem"`
// free form text field for external systems
AdditionalInfo string `json:"additional_info,omitempty"`
}
// GetFilesystem returns the filesystem for this user
@ -849,6 +851,7 @@ func (u *User) getACopy() User {
LastLogin: u.LastLogin,
Filters: filters,
FsConfig: fsConfig,
AdditionalInfo: u.AdditionalInfo,
}
}

View file

@ -73,6 +73,7 @@ For each account, the following properties can be configured:
- `az_upload_concurrency`, how many parts are uploaded in parallel. Zero means the default (2)
- `az_key_prefix`, allows to restrict access to the folder identified by this prefix and its contents
- `az_use_emulator`, boolean
- `additional_info`, string. Free text field
These properties are stored inside the data provider.

View file

@ -44,13 +44,14 @@ Let's see a sample usage for each REST API.
Command:
```console
python sftpgo_api_cli add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --subdirs-permissions "/dir1::list,download" "/dir2::*" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01 --allowed-ip "192.168.1.1/32" --fs S3 --s3-bucket test --s3-region eu-west-1 --s3-access-key accesskey --s3-access-secret secret --s3-endpoint "http://127.0.0.1:9000" --s3-storage-class Standard --s3-key-prefix "vfolder/" --s3-upload-part-size 10 --s3-upload-concurrency 4 --denied-login-methods "password" "keyboard-interactive" --allowed-patterns "/dir1::*.jpg,*.png" "/dir2::*.rar,*.png" --denied-patterns "/dir3::*.zip,*.rar" --denied-protocols DAV FTP
python sftpgo_api_cli add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --subdirs-permissions "/dir1::list,download" "/dir2::*" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01 --allowed-ip "192.168.1.1/32" --fs S3 --s3-bucket test --s3-region eu-west-1 --s3-access-key accesskey --s3-access-secret secret --s3-endpoint "http://127.0.0.1:9000" --s3-storage-class Standard --s3-key-prefix "vfolder/" --s3-upload-part-size 10 --s3-upload-concurrency 4 --denied-login-methods "password" "keyboard-interactive" --allowed-patterns "/dir1::*.jpg,*.png" "/dir2::*.rar,*.png" --denied-patterns "/dir3::*.zip,*.rar" --denied-protocols DAV FTP --additional-info "sample info"
```
Output:
```json
{
"additional_info": "sample info",
"download_bandwidth": 60,
"expiration_date": 1546297200000,
"filesystem": {

View file

@ -84,11 +84,11 @@ class SFTPGoApiRequests:
denied_patterns=[], allowed_patterns=[], s3_upload_part_size=0, s3_upload_concurrency=0,
max_upload_file_size=0, denied_protocols=[], az_container='', az_account_name='', az_account_key='',
az_sas_url='', az_endpoint='', az_upload_part_size=0, az_upload_concurrency=0, az_key_prefix='',
az_use_emulator=False, az_access_tier=''):
az_use_emulator=False, az_access_tier='', additional_info=''):
user = {'id':user_id, 'username':username, 'uid':uid, 'gid':gid,
'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files,
'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth,
'status':status, 'expiration_date':expiration_date}
'status':status, 'expiration_date':expiration_date, 'additional_info':additional_info}
if password is not None:
user.update({'password':password})
if public_keys:
@ -285,7 +285,7 @@ class SFTPGoApiRequests:
denied_login_methods=[], virtual_folders=[], denied_patterns=[], allowed_patterns=[],
s3_upload_part_size=0, s3_upload_concurrency=0, max_upload_file_size=0, denied_protocols=[], az_container="",
az_account_name='', az_account_key='', az_sas_url='', az_endpoint='', az_upload_part_size=0,
az_upload_concurrency=0, az_key_prefix='', az_use_emulator=False, az_access_tier=''):
az_upload_concurrency=0, az_key_prefix='', az_use_emulator=False, az_access_tier='', additional_info=''):
u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key,
@ -293,7 +293,7 @@ class SFTPGoApiRequests:
gcs_credentials_file, gcs_automatic_credentials, denied_login_methods, virtual_folders, denied_patterns,
allowed_patterns, s3_upload_part_size, s3_upload_concurrency, max_upload_file_size, denied_protocols,
az_container, az_account_name, az_account_key, az_sas_url, az_endpoint, az_upload_part_size,
az_upload_concurrency, az_key_prefix, az_use_emulator, az_access_tier)
az_upload_concurrency, az_key_prefix, az_use_emulator, az_access_tier, additional_info)
r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify)
self.printResponse(r)
@ -306,7 +306,7 @@ class SFTPGoApiRequests:
allowed_patterns=[], s3_upload_part_size=0, s3_upload_concurrency=0, max_upload_file_size=0,
denied_protocols=[], disconnect=0, az_container='', az_account_name='', az_account_key='', az_sas_url='',
az_endpoint='', az_upload_part_size=0, az_upload_concurrency=0, az_key_prefix='', az_use_emulator=False,
az_access_tier=''):
az_access_tier='', additional_info=''):
u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, self.buildPermissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date, allowed_ip, denied_ip, fs_provider, s3_bucket, s3_region, s3_access_key,
@ -314,7 +314,7 @@ class SFTPGoApiRequests:
gcs_credentials_file, gcs_automatic_credentials, denied_login_methods, virtual_folders, denied_patterns,
allowed_patterns, s3_upload_part_size, s3_upload_concurrency, max_upload_file_size, denied_protocols,
az_container, az_account_name, az_account_key, az_sas_url, az_endpoint, az_upload_part_size,
az_upload_concurrency, az_key_prefix, az_use_emulator, az_access_tier)
az_upload_concurrency, az_key_prefix, az_use_emulator, az_access_tier, additional_info)
r = requests.put(urlparse.urljoin(self.userPath, 'user/' + str(user_id)), params={'disconnect':disconnect},
json=u, auth=self.auth, verify=self.verify)
self.printResponse(r)
@ -608,6 +608,7 @@ def addCommonUserArguments(parser):
help='User\'s status. 1 enabled, 0 disabled. Default: %(default)s')
parser.add_argument('--max-upload-file-size', type=int, default=0,
help='Maximum allowed size, as bytes, for a single file upload, 0 means unlimited. Default: %(default)s')
parser.add_argument('--additional-info', type=str, default='', help='Free form text field. Default: %(default)s')
parser.add_argument('-E', '--expiration-date', type=validDate, default='',
help='Expiration date as YYYY-MM-DD, empty string means no expiration. Default: %(default)s')
parser.add_argument('-Y', '--allowed-ip', type=str, nargs='+', default=[],
@ -815,7 +816,7 @@ if __name__ == '__main__':
args.s3_upload_part_size, args.s3_upload_concurrency, args.max_upload_file_size, args.denied_protocols,
args.az_container, args.az_account_name, args.az_account_key, args.az_sas_url, args.az_endpoint,
args.az_upload_part_size, args.az_upload_concurrency, args.az_key_prefix, args.az_use_emulator,
args.az_access_tier)
args.az_access_tier, args.additional_info)
elif args.command == 'update-user':
api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid,
args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth,
@ -828,7 +829,7 @@ if __name__ == '__main__':
args.s3_upload_concurrency, args.max_upload_file_size, args.denied_protocols, args.disconnect,
args.az_container, args.az_account_name, args.az_account_key, args.az_sas_url, args.az_endpoint,
args.az_upload_part_size, args.az_upload_concurrency, args.az_key_prefix, args.az_use_emulator,
args.az_access_tier)
args.az_access_tier, args.additional_info)
elif args.command == 'delete-user':
api.deleteUser(args.id)
elif args.command == 'get-users':

View file

@ -704,6 +704,9 @@ func compareAzBlobConfig(expected *dataprovider.User, actual *dataprovider.User)
if expected.FsConfig.AzBlobConfig.UseEmulator != actual.FsConfig.AzBlobConfig.UseEmulator {
return errors.New("Azure Blob use emulator mismatch")
}
if expected.FsConfig.AzBlobConfig.AccessTier != actual.FsConfig.AzBlobConfig.AccessTier {
return errors.New("Azure Blob access tier mismatch")
}
return nil
}
@ -861,6 +864,9 @@ func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.U
if expected.ExpirationDate != actual.ExpirationDate {
return errors.New("ExpirationDate mismatch")
}
if expected.AdditionalInfo != actual.AdditionalInfo {
return errors.New("AdditionalInfo mismatch")
}
return nil
}

View file

@ -243,7 +243,7 @@ func TestBasicUserHandling(t *testing.T) {
user.UploadBandwidth = 128
user.DownloadBandwidth = 64
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now())
user.AdditionalInfo = "some free text"
originalUser := user
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
@ -2715,6 +2715,7 @@ func TestWebUserAddMock(t *testing.T) {
user.UploadBandwidth = 32
user.DownloadBandwidth = 64
user.UID = 1000
user.AdditionalInfo = "info"
mappedDir := filepath.Join(os.TempDir(), "mapped")
form := make(url.Values)
form.Set("username", user.Username)
@ -2729,6 +2730,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("denied_extensions", "/dir2::.webp,.webp\n/dir2::.tiff\n/dir1::.zip")
form.Set("allowed_patterns", "/dir2::*.jpg,*.png\n/dir1::*.png")
form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv")
form.Set("additional_info", user.AdditionalInfo)
b, contentType, _ := getMultipartFormData(form, "", "")
// test invalid url escape
req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b)
@ -2848,6 +2850,7 @@ func TestWebUserAddMock(t *testing.T) {
assert.Equal(t, user.UploadBandwidth, newUser.UploadBandwidth)
assert.Equal(t, user.DownloadBandwidth, newUser.DownloadBandwidth)
assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize)
assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo)
assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys))
if val, ok := newUser.Permissions["/subdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val))
@ -2926,6 +2929,7 @@ func TestWebUserUpdateMock(t *testing.T) {
user.QuotaFiles = 2
user.QuotaSize = 3
user.GID = 1000
user.AdditionalInfo = "new additional info"
form := make(url.Values)
form.Set("username", user.Username)
form.Set("home_dir", user.HomeDir)
@ -2947,6 +2951,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("denied_protocols", common.ProtocolFTP)
form.Set("max_upload_file_size", "100")
form.Set("disconnect", "1")
form.Set("additional_info", user.AdditionalInfo)
b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), &b)
req.Header.Set("Content-Type", contentType)
@ -2966,6 +2971,7 @@ func TestWebUserUpdateMock(t *testing.T) {
assert.Equal(t, user.QuotaSize, updateUser.QuotaSize)
assert.Equal(t, user.UID, updateUser.UID)
assert.Equal(t, user.GID, updateUser.GID)
assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo)
assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize)
if val, ok := updateUser.Permissions["/otherdir"]; ok {

View file

@ -311,6 +311,10 @@ func TestCompareUserFields(t *testing.T) {
expected.ExpirationDate = 123
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.ExpirationDate = 0
expected.AdditionalInfo = "info"
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
}
func TestCompareUserFsConfig(t *testing.T) {
@ -443,6 +447,10 @@ func TestCompareUserAzureConfig(t *testing.T) {
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.UseEmulator = false
expected.FsConfig.AzBlobConfig.AccessTier = "Hot"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.AccessTier = ""
}
func TestGCSWebInvalidFormFile(t *testing.T) {

View file

@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 2.1.0
version: 2.1.1
servers:
- url: /api/v1
@ -1239,6 +1239,9 @@ components:
$ref: '#/components/schemas/UserFilters'
filesystem:
$ref: '#/components/schemas/FilesystemConfig'
additional_info:
type: string
description: Free form text field for external systems
Transfer:
type: object
properties:

View file

@ -589,6 +589,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
ExpirationDate: expirationDateMillis,
Filters: getFiltersFromUserPostFields(r),
FsConfig: fsConfig,
AdditionalInfo: r.Form.Get("additional_info"),
}
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
user.Filters.MaxUploadFileSize = maxFileSize

View file

@ -517,6 +517,17 @@
</div>
</div>
<div class="form-group row">
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
aria-describedby="additionalInfoHelpBlock">{{.User.AdditionalInfo}}</textarea>
<small id="additionalInfoHelpBlock" class="form-text text-muted">
Free form text field
</small>
</div>
</div>
{{if not .IsAdd}}
<div class="form-group">
<div class="form-check">