diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index 93d2d741..ac759128 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -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) +} diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index 1bdad1d4..c06f1b34 100644 --- a/dataprovider/pgsql.go +++ b/dataprovider/pgsql.go @@ -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) +} diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index a5111798..38284fc1 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -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 } diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index 00374448..1d698386 100644 --- a/dataprovider/sqlite.go +++ b/dataprovider/sqlite.go @@ -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) +} diff --git a/dataprovider/sqlqueries.go b/dataprovider/sqlqueries.go index 564531fb..435e8b64 100644 --- a/dataprovider/sqlqueries.go +++ b/dataprovider/sqlqueries.go @@ -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 { diff --git a/dataprovider/user.go b/dataprovider/user.go index 344f61be..a9cfbce7 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -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, } } diff --git a/docs/account.md b/docs/account.md index 6ed7eaa3..be31e3cb 100644 --- a/docs/account.md +++ b/docs/account.md @@ -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. diff --git a/examples/rest-api-cli/README.md b/examples/rest-api-cli/README.md index 4f100afd..933a1d42 100644 --- a/examples/rest-api-cli/README.md +++ b/examples/rest-api-cli/README.md @@ -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": { diff --git a/examples/rest-api-cli/sftpgo_api_cli b/examples/rest-api-cli/sftpgo_api_cli index 55661dcc..dc79db0c 100755 --- a/examples/rest-api-cli/sftpgo_api_cli +++ b/examples/rest-api-cli/sftpgo_api_cli @@ -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': diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 37e6e303..66ce925b 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -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 } diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index ec213028..d1916433 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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 { diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 5a7b93c5..2852752f 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -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) { diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 45f80854..b7561b9a 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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: diff --git a/httpd/web.go b/httpd/web.go index b76d9925..00d9457e 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -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 diff --git a/templates/user.html b/templates/user.html index dccca447..30694550 100644 --- a/templates/user.html +++ b/templates/user.html @@ -517,6 +517,17 @@ +
+ +
+ + + Free form text field + +
+
+ {{if not .IsAdd}}