diff --git a/docs/design/iam.md b/docs/design/iam.md index 2b2a8c32bd..c5edb2384d 100644 --- a/docs/design/iam.md +++ b/docs/design/iam.md @@ -88,3 +88,18 @@ Source: AccessKeys - AccessKey (Create, Update, Delete, List) - root account - all IAM users only for themselves (except the first creation that can be done only by the root account). + +### Root Accounts Manager +The root accounts managers are a solution for creating root accounts using the IAM API. + +- The root accounts managers will be created only using the CLI (can have more than one root account manager). +- It is not mandatory to have a root account manager, it is only for allowing the IAM API for creating new root accounts, but this account does not owns the root accounts. +- The root accounts manager functionality is like root account in the IAM API perspective: + - We use root accounts to create IAM users: We use root accounts manager to create root accounts + - We use root accounts to create the first access key of an IAM user: We use root accounts manager to create the first access key of a root account. +- When using IAM users API: + - root accounts manager can run IAM users create/update/delete/list - only on root accounts (not on other IAM users). +root accounts manager can run IAM access keys create/update/delete/list - only on root accounts and himself. + +Here attached a diagram with all the accounts that we have in our system: +![All accounts diagram](https://github.com/noobaa/noobaa-core/assets/57721533/c4395c06-3ab3-4425-838b-c020ef7cc38a) \ No newline at end of file diff --git a/docs/dev_guide/nc_nsfs_iam_developer_doc.md b/docs/dev_guide/nc_nsfs_iam_developer_doc.md index a684f60383..91d5ca4a54 100644 --- a/docs/dev_guide/nc_nsfs_iam_developer_doc.md +++ b/docs/dev_guide/nc_nsfs_iam_developer_doc.md @@ -13,7 +13,7 @@ This will be the argument for: - `new_buckets_path` flag `/tmp/nsfs_root1` (that we will use in the account commands) - `path` in the buckets commands `/tmp/nsfs_root1/my-bucket` (that we will use in bucket commands). 2. Create the root user account with the CLI: -`sudo node src/cmd/manage_nsfs account add --name > --new_buckets_path /tmp/nsfs_root1 --access_key --secret_key --uid --gid `. +`sudo node src/cmd/manage_nsfs account add --name --new_buckets_path /tmp/nsfs_root1 --access_key --secret_key --uid --gid `. 3. Start the NSFS server (using debug mode and the port for IAM): `sudo node src/cmd/nsfs --debug 5 --https_port_iam 7005` Note: before starting the server please add this line: `process.env.NOOBAA_LOG_LEVEL = 'nsfs';` in the endpoint.js (before the condition `if (process.env.NOOBAA_LOG_LEVEL) {`) 4. Create the alias for IAM service: @@ -40,7 +40,7 @@ Create the alias for IAM service for the user that was created (with its access 1. Use the root account credentials to create a user: `nc-user-1-iam iam create-user --user-name ` 2. Use the root account credentials to create access keys for the user: `nc-user-1-iam iam create-access-key --user-name ` 3. The alias for s3 service: `alias nc-user-1-s3-regular='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:6443'` -2. Create a bucket (so we can list it) `nc-user-1-s3-regular s3 mb s3://>` +2. Create a bucket (so we can list it) `nc-user-1-s3-regular s3 mb s3:// --user-name --status Inactive` @@ -51,4 +51,13 @@ Note: Currently we clean the cache after update, but it happens for the specific 1. Use the root account credentials to create a user: `nc-user-1-iam iam create-user --user-name ` (You should see the config file in under the accounts directory). 2. Use the root account credentials to create access keys for the user:(first time): `nc-user-1-iam iam create-access-key --user-name ` (You should see the first symbolic link in under the access_keys directory). 3. Use the root account credentials to create access keys for the user (second time): `nc-user-1-iam iam create-access-key --user-name ` (You should see the second symbolic link in under the access_keys directory). -4. Update the username: `nc-user-1-iam iam update-user --user-name --new-user-name ` (You should see the following changes: config file name updated, symlinks updated according to the current config). \ No newline at end of file +4. Update the username: `nc-user-1-iam iam update-user --user-name --new-user-name ` (You should see the following changes: config file name updated, symlinks updated according to the current config). + +#### Create root account using the IAM API (requesting account is root accounts manager): +1. Create the root accounts manager with the CLI: +`sudo node src/cmd/manage_nsfs account add --name --new_buckets_path /tmp/nsfs_root1 --access_key --secret_key --uid --gid --iam_operate_on_root_account`. +2. Use the root accounts manager details in the alias: +`alias nc-user-manager-iam='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:7005'`. +3. Use the root accounts manager account credentials to create a root account: + `nc-user-manager-iam create-user --user-name ` +4. Use the root account credentials to create access keys for the root account: `nc-user-manager-iam iam create-access-key --user-name ` diff --git a/src/cmd/manage_nsfs.js b/src/cmd/manage_nsfs.js index 2602a84df7..a5b3c37b0c 100644 --- a/src/cmd/manage_nsfs.js +++ b/src/cmd/manage_nsfs.js @@ -341,6 +341,8 @@ async function fetch_account_data(action, user_input) { new_access_key, access_keys, force_md5_etag: _.isUndefined(user_input.force_md5_etag) || user_input.force_md5_etag === '' ? user_input.force_md5_etag : get_boolean_or_string_value(user_input.force_md5_etag), + iam_operate_on_root_account: _.isUndefined(user_input.iam_operate_on_root_account) ? + undefined : get_boolean_or_string_value(user_input.iam_operate_on_root_account), nsfs_account_config: { distinguished_name: user_input.user, uid: user_input.user ? undefined : user_input.uid, @@ -371,7 +373,6 @@ async function fetch_account_data(action, user_input) { data.nsfs_account_config.new_buckets_path = data.nsfs_account_config.new_buckets_path || undefined; // force_md5_etag deletion specified with empty string '' checked against user_input.force_md5_etag because data.force_md5_etag is boolean data.force_md5_etag = data.force_md5_etag === '' ? undefined : data.force_md5_etag; - // allow_bucket_creation either set by user or infer from new_buckets_path if (_.isUndefined(user_input.allow_bucket_creation)) { data.allow_bucket_creation = !_.isUndefined(data.nsfs_account_config.new_buckets_path); } else if (typeof user_input.allow_bucket_creation === 'boolean') { @@ -421,7 +422,7 @@ async function fetch_existing_account_data(action, target, decrypt_secret_key) { } async function add_account(data) { - await manage_nsfs_validations.validate_account_args(data, ACTIONS.ADD); + await manage_nsfs_validations.validate_account_args(data, ACTIONS.ADD, config_root_backend, accounts_dir_path, undefined); const fs_context = native_fs_utils.get_process_fs_context(config_root_backend); const access_key = has_access_keys(data.access_keys) ? data.access_keys[0].access_key : undefined; @@ -456,8 +457,9 @@ async function add_account(data) { } -async function update_account(data) { - await manage_nsfs_validations.validate_account_args(data, ACTIONS.UPDATE); +async function update_account(data, is_flag_iam_operate_on_root_account) { + await manage_nsfs_validations.validate_account_args(data, ACTIONS.UPDATE, + config_root_backend, accounts_dir_path, is_flag_iam_operate_on_root_account); const fs_context = native_fs_utils.get_process_fs_context(config_root_backend); const cur_name = data.name; @@ -521,7 +523,7 @@ async function update_account(data) { } async function delete_account(data) { - await manage_nsfs_validations.validate_account_args(data, ACTIONS.DELETE); + await manage_nsfs_validations.validate_account_args(data, ACTIONS.DELETE, config_root_backend, accounts_dir_path, undefined); await manage_nsfs_validations.validate_delete_account(config_root_backend, buckets_dir_path, data.name); const fs_context = native_fs_utils.get_process_fs_context(config_root_backend); @@ -535,7 +537,7 @@ async function delete_account(data) { } async function get_account_status(data, show_secrets) { - await manage_nsfs_validations.validate_account_args(data, ACTIONS.STATUS); + await manage_nsfs_validations.validate_account_args(data, ACTIONS.STATUS, config_root_backend, accounts_dir_path, undefined); try { const account_path = _.isUndefined(data.name) ? get_symlink_config_file_path(access_keys_dir_path, data.access_keys[0].access_key) : @@ -559,7 +561,8 @@ async function manage_account_operations(action, data, show_secrets, user_input) } else if (action === ACTIONS.STATUS) { await get_account_status(data, show_secrets); } else if (action === ACTIONS.UPDATE) { - await update_account(data); + const is_flag_iam_operate_on_root_account = get_boolean_or_string_value(user_input.iam_operate_on_root_account); + await update_account(data, is_flag_iam_operate_on_root_account); } else if (action === ACTIONS.DELETE) { await delete_account(data); } else if (action === ACTIONS.LIST) { diff --git a/src/manage_nsfs/manage_nsfs_cli_errors.js b/src/manage_nsfs/manage_nsfs_cli_errors.js index e9b21d10d2..db1c22ce76 100644 --- a/src/manage_nsfs/manage_nsfs_cli_errors.js +++ b/src/manage_nsfs/manage_nsfs_cli_errors.js @@ -198,6 +198,20 @@ ManageCLIError.AccountDeleteForbiddenHasBuckets = Object.freeze({ http_code: 403, }); +ManageCLIError.AccountCannotCreateRootAccountsRequesterIAMUser = Object.freeze({ + code: 'AccountCannotCreateRootAccounts', + message: 'Cannot update account to have iam_operate_on_root_account. ' + + 'You must use root account for this action', + http_code: 409, +}); + +ManageCLIError.AccountCannotBeRootAccountsManager = Object.freeze({ + code: 'AccountCannotBeRootAccountsManager', + message: 'Cannot update account to have iam_operate_on_root_account. ' + + 'You must delete all IAM accounts before update or ' + + 'use root accounts that does not owns any IAM accounts', + http_code: 409, +}); ////////////////////////////////// //// ACCOUNT ARGUMENTS ERRORS //// diff --git a/src/manage_nsfs/manage_nsfs_cli_utils.js b/src/manage_nsfs/manage_nsfs_cli_utils.js index 16627655e3..ec7ea6e9ac 100644 --- a/src/manage_nsfs/manage_nsfs_cli_utils.js +++ b/src/manage_nsfs/manage_nsfs_cli_utils.js @@ -142,6 +142,17 @@ function generate_id() { return mongo_utils.mongoObjectId(); } +/** + * check_root_account_owns_user checks if an account is owned by root account + * @param {object} root_account + * @param {object} account + */ +function check_root_account_owns_user(root_account, account) { + if (account.owner === undefined) return false; + return root_account._id === account.owner; +} + + // EXPORTS exports.throw_cli_error = throw_cli_error; exports.write_stdout_response = write_stdout_response; @@ -154,3 +165,4 @@ exports.get_options_from_file = get_options_from_file; exports.has_access_keys = has_access_keys; exports.generate_id = generate_id; exports.set_debug_level = set_debug_level; +exports.check_root_account_owns_user = check_root_account_owns_user; diff --git a/src/manage_nsfs/manage_nsfs_constants.js b/src/manage_nsfs/manage_nsfs_constants.js index 9f26efa88e..6add8fb916 100644 --- a/src/manage_nsfs/manage_nsfs_constants.js +++ b/src/manage_nsfs/manage_nsfs_constants.js @@ -35,8 +35,8 @@ const FROM_FILE = 'from_file'; const ANONYMOUS = 'anonymous'; const VALID_OPTIONS_ACCOUNT = { - 'add': new Set(['name', 'uid', 'gid', 'new_buckets_path', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', FROM_FILE, ...GLOBAL_CONFIG_OPTIONS]), - 'update': new Set(['name', 'uid', 'gid', 'new_buckets_path', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'new_name', 'regenerate', ...GLOBAL_CONFIG_OPTIONS]), + 'add': new Set(['name', 'uid', 'gid', 'new_buckets_path', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', FROM_FILE, ...GLOBAL_CONFIG_OPTIONS]), + 'update': new Set(['name', 'uid', 'gid', 'new_buckets_path', 'user', 'access_key', 'secret_key', 'fs_backend', 'allow_bucket_creation', 'force_md5_etag', 'iam_operate_on_root_account', 'new_name', 'regenerate', ...GLOBAL_CONFIG_OPTIONS]), 'delete': new Set(['name', ...GLOBAL_CONFIG_OPTIONS]), 'list': new Set(['wide', 'show_secrets', 'gid', 'uid', 'user', 'name', 'access_key', ...GLOBAL_CONFIG_OPTIONS]), 'status': new Set(['name', 'access_key', 'show_secrets', ...GLOBAL_CONFIG_OPTIONS]), @@ -91,6 +91,7 @@ const OPTION_TYPE = { fs_backend: 'string', allow_bucket_creation: 'boolean', force_md5_etag: 'boolean', + iam_operate_on_root_account: 'boolean', config_root: 'string', from_file: 'string', config_root_backend: 'string', @@ -113,7 +114,7 @@ const OPTION_TYPE = { const BOOLEAN_STRING_VALUES = ['true', 'false']; const BOOLEAN_STRING_OPTIONS = new Set(['allow_bucket_creation', 'regenerate', 'wide', 'show_secrets', 'force', - 'force_md5_etag', 'all_account_details', 'all_bucket_details', 'anonymous']); + 'force_md5_etag', 'iam_operate_on_root_account', 'all_account_details', 'all_bucket_details', 'anonymous']); //options that can be unset using '' const LIST_UNSETABLE_OPTIONS = ['fs_backend', 's3_policy', 'force_md5_etag']; diff --git a/src/manage_nsfs/manage_nsfs_help_utils.js b/src/manage_nsfs/manage_nsfs_help_utils.js index 9ab398c834..a04085bdf0 100644 --- a/src/manage_nsfs/manage_nsfs_help_utils.js +++ b/src/manage_nsfs/manage_nsfs_help_utils.js @@ -80,6 +80,7 @@ Flags: --fs_backend (optional) Set the filesystem type of new_buckets_path (default config.NSFS_NC_STORAGE_BACKEND) --allow_bucket_creation (optional) Set the account to explicitly allow or block bucket creation --force_md5_etag (optional) Set the account to force md5 etag calculation. (unset with '') (will override default config.NSFS_NC_STORAGE_BACKEND) +--iam_operate_on_root_account (optional) Set the account to create root accounts instead of IAM users in IAM API requests. --from_file (optional) Use details from the JSON file, there is no need to mention all the properties individually in the CLI `; @@ -100,6 +101,7 @@ Flags: --fs_backend (optional) Update the filesystem type of new_buckets_path (default config.NSFS_NC_STORAGE_BACKEND) --allow_bucket_creation (optional) Update the account to explicitly allow or block bucket creation --force_md5_etag (optional) Update the account to force md5 etag calculation (unset with '') (will override default config.NSFS_NC_STORAGE_BACKEND) +--iam_operate_on_root_account (optional) Update the account to create root accounts instead of IAM users in IAM API requests. `; const ACCOUNT_FLAGS_DELETE = ` diff --git a/src/manage_nsfs/manage_nsfs_validations.js b/src/manage_nsfs/manage_nsfs_validations.js index e6ea6de64b..91a99b7a0e 100644 --- a/src/manage_nsfs/manage_nsfs_validations.js +++ b/src/manage_nsfs/manage_nsfs_validations.js @@ -13,7 +13,8 @@ const native_fs_utils = require('../util/native_fs_utils'); const ManageCLIError = require('../manage_nsfs/manage_nsfs_cli_errors').ManageCLIError; const bucket_policy_utils = require('../endpoint/s3/s3_bucket_policy_utils'); const { throw_cli_error, get_config_file_path, get_bucket_owner_account, - get_config_data, get_options_from_file, get_boolean_or_string_value } = require('../manage_nsfs/manage_nsfs_cli_utils'); + get_config_data, get_options_from_file, get_boolean_or_string_value, + check_root_account_owns_user } = require('../manage_nsfs/manage_nsfs_cli_utils'); const { TYPES, ACTIONS, VALID_OPTIONS, OPTION_TYPE, FROM_FILE, BOOLEAN_STRING_VALUES, BOOLEAN_STRING_OPTIONS, GLACIER_ACTIONS, LIST_UNSETABLE_OPTIONS, ANONYMOUS } = require('../manage_nsfs/manage_nsfs_constants'); @@ -361,7 +362,8 @@ function validate_account_identifier(action, input_options) { * @param {object} data * @param {string} action */ -async function validate_account_args(data, action) { +async function validate_account_args(data, action, config_root_backend, accounts_dir_path, + is_flag_iam_operate_on_root_account_update_action) { if (action === ACTIONS.ADD || action === ACTIONS.UPDATE) { if (data.nsfs_account_config.gid && data.nsfs_account_config.uid === undefined) { throw_cli_error(ManageCLIError.MissingAccountNSFSConfigUID, data.nsfs_account_config); @@ -392,6 +394,9 @@ async function validate_account_args(data, action) { if (!accessible) { throw_cli_error(ManageCLIError.InaccessibleAccountNewBucketsPath, data.nsfs_account_config.new_buckets_path); } + if (action === ACTIONS.UPDATE && is_flag_iam_operate_on_root_account_update_action) { + await validate_root_accounts_manager_update(config_root_backend, accounts_dir_path, data); + } } } @@ -435,6 +440,41 @@ async function validate_delete_account(config_root_backend, buckets_dir_path, ac }); } + // TODO - when we have the structure of config we can check easily which IAM users are owned by the root account + // currently, partial copy from _list_config_files_for_users + async function check_if_root_account_does_not_have_IAM_users(config_root_backend, accounts_dir_path, account_to_check) { + const fs_context = native_fs_utils.get_process_fs_context(config_root_backend); + const entries = await nb_native().fs.readdir(fs_context, accounts_dir_path); + await P.map_with_concurrency(10, entries, async entry => { + if (entry.name.endsWith('.json')) { + const full_path = path.join(accounts_dir_path, entry.name); + const account_data = await get_config_data(config_root_backend, full_path); + if (entry.name.includes(config.NSFS_TEMP_CONF_DIR_NAME)) return undefined; + const is_root_account_owns_user = check_root_account_owns_user(account_to_check, account_data); + if (is_root_account_owns_user) { + const detail_msg = `Account ${account_to_check.name} has IAM account ${account_data.name}`; + throw_cli_error(ManageCLIError.AccountCannotBeRootAccountsManager, detail_msg); + } + return account_data; + } + }); + } + +/** + * validate_root_accounts_manager_update checks that an updated account that was set with iam_operate_on_root_account true: + * 1 - is not an IAM user + * 2 - the account does not owns IAM users + * @param {string} config_root_backend + * @param {string} accounts_dir_path + * @param {object} account + */ +async function validate_root_accounts_manager_update(config_root_backend, accounts_dir_path, account) { + if (account.owner) { + throw_cli_error(ManageCLIError.AccountCannotCreateRootAccountsRequesterIAMUser); + } + await check_if_root_account_does_not_have_IAM_users(config_root_backend, accounts_dir_path, account); +} + /////////////////////////////////// //// IP WhITE LIST VALIDATIONS //// /////////////////////////////////// @@ -462,6 +502,7 @@ exports.validate_bucket_args = validate_bucket_args; exports.validate_account_args = validate_account_args; exports._validate_access_keys = _validate_access_keys; exports.validate_delete_account = validate_delete_account; +exports.validate_root_accounts_manager_update = validate_root_accounts_manager_update; exports.validate_whitelist_arg = validate_whitelist_arg; exports.validate_whitelist_ips = validate_whitelist_ips; exports.validate_flags_combination = validate_flags_combination; diff --git a/src/sdk/accountspace_fs.js b/src/sdk/accountspace_fs.js index d7db101383..ebf2e0bb8b 100644 --- a/src/sdk/accountspace_fs.js +++ b/src/sdk/accountspace_fs.js @@ -95,8 +95,11 @@ class AccountSpaceFS { // 2 - find the username (flag username is not required) // 3 - check that the user account config file exists // 4 - read the account config file (no decryption) - // 5 - check that the user to get is not a root account - // 6 - check that the user account to get is owned by the root account + // if the requesting account is root account that creates IAM user: + // 5 - check that the user to get is not a root account + // 6 - check that the user account to get is owned by the root account + // if the requesting account is root accounts manager that creates root account user: + // 5 - check that the user to get is not an IAM user async get_user(params, account_sdk) { const action = 'get_user'; dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); @@ -110,7 +113,7 @@ class AccountSpaceFS { const account_config_path = this._get_account_config_path(username); await this._check_if_account_config_file_exists(action, username, account_config_path); const account_to_get = await this._get_account_decrypted_data_optional(account_config_path, false); - this._check_if_requested_account_is_root_account(action, requesting_account, account_to_get, params); + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, account_to_get); this._check_if_requested_is_owned_by_root_account(action, requesting_account, account_to_get); return { user_id: account_to_get._id, @@ -129,8 +132,11 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account // 2 - check that the user account config file exists // 3 - read the account config file (and decrypt its existing encrypted secret keys and then encrypted secret keys) - // 4 - check that the user to update is not a root account - // 5 - check that the user account to get is owned by the root account + // if the requesting account is root account that creates IAM user: + // 4 - check that the user to get is not a root account + // 5 - check that the user account to get is owned by the root account + // if the requesting account is root accounts manager that creates root account user: + // 4, 5 - check that the user to get is not an IAM user // 6 - check if username was updated // 6.1 - check if username already exists (global scope - all config files names) // 6.2 - create the new config file (with the new name same data) and delete the the existing config file @@ -147,7 +153,7 @@ class AccountSpaceFS { const account_config_path = this._get_account_config_path(params.username); await this._check_if_account_config_file_exists(action, params.username, account_config_path); const requested_account = await this._get_account_decrypted_data_optional(account_config_path, false); - this._check_if_requested_account_is_root_account(action, requesting_account, requested_account, params); + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); this._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account); requested_account.access_keys = await nc_mkm.decrypt_access_keys(requested_account); const is_username_update = !_.isUndefined(params.new_username) && @@ -180,9 +186,14 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account // 2 - check that the user account config file exists // 3 - read the account config file (no decryption) - // 4 - check that the deleted user is not a root account - // 5 - check that the deleted user is owned by the root account - // 6 - check if the user doesn’t have resources related to it (in IAM users only access keys) + // if the requesting account is root account that creates IAM user: + // 4 - check that the user to get is not a root account + // 5 - check that the user account to get is owned by the root account + // if the requesting account is root accounts manager that creates root account user: + // 4, 5 - check that the user to get is not an IAM user + // 6 - check if the user doesn’t have resources related to it: + // in IAM users only access keys + // in root accounts it can be: IAM users, buckets and access keys // note: buckets are owned by the root account // 7 - delete the account config file async delete_user(params, account_sdk) { @@ -196,9 +207,9 @@ class AccountSpaceFS { const account_config_path = this._get_account_config_path(params.username); await this._check_if_account_config_file_exists(action, params.username, account_config_path); const account_to_delete = await this._get_account_decrypted_data_optional(account_config_path, false); - this._check_if_requested_account_is_root_account(action, requesting_account, account_to_delete, params); + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, account_to_delete); this._check_if_requested_is_owned_by_root_account(action, requesting_account, account_to_delete); - this._check_if_user_does_not_have_access_keys_before_deletion(action, account_to_delete); + await this._check_if_user_does_not_have_resources_before_deletion(action, account_to_delete); await native_fs_utils.delete_config_file(this.fs_context, this.accounts_dir, account_config_path); } catch (err) { dbg.error(`AccountSpaceFS.${action} error`, err); @@ -207,7 +218,11 @@ class AccountSpaceFS { } // 1 - check that the requesting account is a root user account - // 2 - list the config files that are owned by the root user account + // if the requesting account is root account that creates IAM user: + // 2 - list the config files that are owned by the root user account + // if the requesting account is root accounts manager that creates root account user: + // 2 - list the config files of the root accounts + // Note: will always have at least 1 account (himself) // 2.1 - if the request has path_prefix check if the user’s path starts with this path // 3- sort the members by username (a to z) async list_users(params, account_sdk) { @@ -233,8 +248,8 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account or that the username is same as the requester // 2 - check that the requested account config file exists // 3 - read the account config file (and decrypt its existing encrypted secret keys and then encrypted secret keys) - // 4 - if the requester is root user account - check that it owns the account - // check that the access key to create is on a user is owned by the the root account + // 4 - if the requesting account is root account - check that the access key to create is on a user is owned by the the root account + // if the requesting account is root accounts manager - check that it performs on root account and not IAM user // 5 - check that the number of access key array // 6 - generate access keys // 7 - encryption @@ -254,6 +269,9 @@ class AccountSpaceFS { const requested_account = await this._get_account_decrypted_data_optional(requested_account_config_path, true); if (requester.identity === identity_enum.ROOT_ACCOUNT) { this._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account); + if (requesting_account.iam_operate_on_root_account) { + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + } } this._check_number_of_access_key_array(action, requested_account); const { generated_access_key, generated_secret_key } = this._generate_access_key(); @@ -286,7 +304,8 @@ class AccountSpaceFS { // 1 - read the symlink file that we get in params (access key id) // 2 - check if the access key that was received in param exists // 3 - read the config file - // 4 - check that config file is on the same root account + // 4 - if the requesting account is root account - check that config file is on the same root account + // if the requesting account is root accounts manager - check that it performs on root account and not IAM user // General note: only serves the requester (no flag --user-name is passed) async get_access_key_last_used(params, account_sdk) { const action = 'get_access_key_last_used'; @@ -295,10 +314,13 @@ class AccountSpaceFS { const requesting_account = account_sdk.requesting_account; const access_key_id = params.access_key; const requested_account_path = get_symlink_config_file_path(this.access_keys_dir, access_key_id); - await this._check_if_account_exists_by_access_key_symlink(action, requesting_account, requested_account_path, access_key_id); + await this._check_if_account_exists_by_access_key_symlink(action, requested_account_path, access_key_id); const requested_account = await get_config_data(this.config_root_backend, requested_account_path, true); this._check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account); + if (requesting_account.iam_operate_on_root_account) { + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + } return { region: dummy_region, // GAP last_used_date: new Date(), // GAP @@ -314,13 +336,15 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account or that the username is same as the requester // 2 - check if the access key that was received in param exists // 3 - read the config file (and decrypt the encrypted secret keys) - // 4 - check that config file is on the same root account - // 5 - check if we need to change the status (if not - return) - // 6 - update the access key status (Active/Inactive) - // 7 - encryption - // 8 - validate account - // 9 - update account config file - // 10 - remove the access_key from the account_cache + // 4 - check if the access key id belongs to the account + // 5 - if the requesting account is root account - check that the access key to update is on a user is owned by the the root account + // if the requesting account is root accounts manager - check that it performs on root account and not IAM user + // 6 - check if we need to change the status (if not - return) + // 7 - update the access key status (Active/Inactive) + // 8 - encryption + // 9 - validate account + // 10 - update account config file + // 11 - remove the access_key from the account_cache async update_access_key(params, account_sdk) { const action = 'update_access_key'; dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); @@ -330,10 +354,14 @@ class AccountSpaceFS { const requester = this._check_if_requesting_account_is_root_account_or_user_om_himself(action, requesting_account, params.username); const requested_account_path = get_symlink_config_file_path(this.access_keys_dir, params.access_key); - await this._check_if_account_exists_by_access_key_symlink(action, requesting_account, requested_account_path, access_key_id); + await this._check_if_account_exists_by_access_key_symlink(action, requested_account_path, access_key_id); const requested_account = await this._get_account_decrypted_data_optional(requested_account_path, true); + this._check_access_key_belongs_to_account(action, requested_account, access_key_id); this._check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account); + if (requesting_account.iam_operate_on_root_account) { + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + } const access_key_obj = _.find(requested_account.access_keys, access_key => access_key.access_key === access_key_id); if (this._get_access_key_status(access_key_obj.deactivated) === params.status) { // note: master key might be changed and we do not update it since we do not update the config file @@ -359,13 +387,15 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account or that the username is same as the requester // 2 - check if the access key that was received in param exists // 3 - read the config file (and decrypt the encrypted secret keys) - // 4 - check that config file is on the same root account - // 5 - delete the access key object (access key, secret key, status, etc.) from the array - // 6 - encryption (of existing access keys) - // 7 - validate account - // 8 - update account config file - // 9 - unlink the symbolic link - // 10 - remove the access_key from the account_cache + // 4 - check if the access key id belongs to the account + // 5 - if the requesting account is root account - check that the access key to delete is on a user is owned by the the root account + // if the requesting account is root accounts manager - check that it performs on root account and not IAM user + // 6 - delete the access key object (access key, secret key, status, etc.) from the array + // 7 - encryption (of existing access keys) + // 8 - validate account + // 9 - update account config file + // 10 - unlink the symbolic link + // 11 - remove the access_key from the account_cache async delete_access_key(params, account_sdk) { const action = 'delete_access_key'; dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); @@ -375,10 +405,14 @@ class AccountSpaceFS { const requester = this._check_if_requesting_account_is_root_account_or_user_om_himself(action, requesting_account, params.username); const requested_account_path = get_symlink_config_file_path(this.access_keys_dir, access_key_id); - await this._check_if_account_exists_by_access_key_symlink(action, requesting_account, requested_account_path, access_key_id); + await this._check_if_account_exists_by_access_key_symlink(action, requested_account_path, access_key_id); const requested_account = await this._get_account_decrypted_data_optional(requested_account_path, true); + this._check_access_key_belongs_to_account(action, requested_account, access_key_id); this._check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account); + if (requesting_account.iam_operate_on_root_account) { + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + } requested_account.access_keys = requested_account.access_keys.filter(access_key_obj => access_key_obj.access_key !== access_key_id); const requested_account_encrypted = await nc_mkm.encrypt_access_keys(requested_account); @@ -399,7 +433,8 @@ class AccountSpaceFS { // 1 - check that the requesting account is a root user account or that the username is same as the requester // 2 - check that the user account config file exists // 3 - read the account config file (no decryption) - // 4 - check that config file is on the same root account + // 4 - if the requesting account is root account - check that the access key to delete is on a user is owned by the the root account + // if the requesting account is root accounts manager - check that it performs on root account and not IAM user // 5 - list the access-keys // 6 - members should be sorted by access_key (a to z) // GAP - this is not written in the docs, only inferred (maybe it sorted is by create_date?) @@ -416,6 +451,9 @@ class AccountSpaceFS { const requested_account = await this._get_account_decrypted_data_optional(requested_account_config_path, false); this._check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account); + if (requesting_account.iam_operate_on_root_account) { + this._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + } const is_truncated = false; // path_prefix is not supported let members = this._list_access_keys_from_account(requested_account); members = members.sort((a, b) => a.access_key.localeCompare(b.access_key)); @@ -458,7 +496,7 @@ class AccountSpaceFS { _new_user_defaults(requesting_account, params, master_key_id) { const distinguished_name = requesting_account.nsfs_account_config.distinguished_name; - return { + const user_defaults = { _id: generate_id(), name: params.username, email: params.username, @@ -478,6 +516,15 @@ class AccountSpaceFS { fs_backend: requesting_account.nsfs_account_config.fs_backend, } }; + if (requesting_account.iam_operate_on_root_account) { + dbg.log2('_new_user_defaults creates root account user'); + delete user_defaults.owner; + // set the allow bucket creation to true if we have new_buckets_path + if (!user_defaults.allow_bucket_creation && user_defaults.nsfs_account_config.new_buckets_path) { + user_defaults.allow_bucket_creation = true; + } + } + return user_defaults; } _check_root_account(account) { @@ -530,6 +577,21 @@ class AccountSpaceFS { throw new IamError({ code, message: message_with_details, http_code, type }); } + // TODO: move to IamError class with a template + _throw_error_delete_conflict(action, account_to_delete, resource_name) { + dbg.error(`AccountSpaceFS.${action} requested account ` + + `${account_to_delete.name} ${account_to_delete._id} has ${resource_name}`); + const message_with_details = `Cannot delete entity, must delete ${resource_name} first.`; + const { code, http_code, type } = IamError.DeleteConflict; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + + _throw_error_perform_action_from_root_accounts_manager_on_iam_user(action, requesting_account, requested_account) { + dbg.error(`AccountSpaceFS.${action} root accounts manager cannot perform actions on IAM users`, + requesting_account, requested_account); + throw new IamError(IamError.NotAuthorized); + } + // based on the function from manage_nsfs async _list_config_files_for_users(requesting_account, iam_path_prefix) { const entries = await nb_native().fs.readdir(this.fs_context, this.accounts_dir); @@ -540,7 +602,9 @@ class AccountSpaceFS { const full_path = path.join(this.accounts_dir, entry.name); const account_data = await this._get_account_decrypted_data_optional(full_path, false); if (entry.name.includes(config.NSFS_TEMP_CONF_DIR_NAME)) return undefined; - if (this._check_root_account_owns_user(requesting_account, account_data)) { + const is_root_account_owns_user = this._check_root_account_owns_user(requesting_account, account_data); + if ((!requesting_account.iam_operate_on_root_account && is_root_account_owns_user) || + (requesting_account.iam_operate_on_root_account && this._check_root_account(account_data))) { if (should_filter_by_prefix) { if (account_data.iam_path === undefined) return undefined; if (!account_data.iam_path.startsWith(iam_path_prefix)) return undefined; @@ -573,12 +637,19 @@ class AccountSpaceFS { } } - _check_if_requested_account_is_root_account(action, requesting_account, requested_account) { + _check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account) { const is_requested_account_root_account = this._check_root_account(requested_account); dbg.log1(`AccountSpaceFS.${action} requested_account`, requested_account, 'is_requested_account_root_account', is_requested_account_root_account); - if (is_requested_account_root_account) { - this._throw_error_perform_action_on_another_root_account(action, requesting_account, requested_account); + // access to root account is allowed to root account that has iam_operate_on_root_account true + if (is_requested_account_root_account && !requesting_account.iam_operate_on_root_account) { + this._throw_error_perform_action_on_another_root_account(action, + requesting_account, requested_account); + } + // access to IAM user is allowed to root account that either iam_operate_on_root_account undefined or false + if (requesting_account.iam_operate_on_root_account && !is_requested_account_root_account) { + this._throw_error_perform_action_from_root_accounts_manager_on_iam_user(action, + requesting_account, requested_account); } } @@ -618,6 +689,7 @@ class AccountSpaceFS { } _check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account) { + if (requesting_account.iam_operate_on_root_account) return; const is_user_account_to_get_owned_by_root_user = this._check_root_account_owns_user(requesting_account, requested_account); if (!is_user_account_to_get_owned_by_root_user) { dbg.error(`AccountSpaceFS.${action} requested account is not owned by root account`, @@ -628,14 +700,58 @@ class AccountSpaceFS { } } + async _check_if_user_does_not_have_resources_before_deletion(action, account_to_delete) { + const is_account_to_delete_root_account = this._check_root_account(account_to_delete); + if (is_account_to_delete_root_account) { + await this._check_if_root_account_does_not_have_buckets_before_deletion(action, account_to_delete); + await this._check_if_root_account_does_not_have_IAM_users_before_deletion(action, account_to_delete); + } + this._check_if_user_does_not_have_access_keys_before_deletion(action, account_to_delete); + } + + // TODO - when we have the structure of config we can check easily which buckets are owned by the root account + // currently, partial copy from verify_account_not_owns_bucket + async _check_if_root_account_does_not_have_buckets_before_deletion(action, account_to_delete) { + const resource_name = 'buckets'; + const entries = await nb_native().fs.readdir(this.fs_context, this.buckets_dir); + await P.map_with_concurrency(10, entries, async entry => { + if (entry.name.endsWith('.json')) { + const full_path = path.join(this.buckets_dir, entry.name); + const bucket_data = await get_config_data(this.config_root_backend, full_path, false); + if (bucket_data.bucket_owner === account_to_delete.name) { + this._throw_error_delete_conflict(action, account_to_delete, resource_name); + } + return bucket_data; + } + }); + } + + // TODO - when we have the structure of config we can check easily which IAM users are owned by the root account + // currently, partial copy from _list_config_files_for_users + async _check_if_root_account_does_not_have_IAM_users_before_deletion(action, account_to_delete) { + const resource_name = 'IAM users'; + const entries = await nb_native().fs.readdir(this.fs_context, this.accounts_dir); + await P.map_with_concurrency(10, entries, async entry => { + if (entry.name.endsWith('.json')) { + const full_path = path.join(this.accounts_dir, entry.name); + const account_data = await this._get_account_decrypted_data_optional(full_path, false); + if (entry.name.includes(config.NSFS_TEMP_CONF_DIR_NAME)) return undefined; + const is_root_account_owns_user = this._check_root_account_owns_user(account_to_delete, account_data); + if ((!account_to_delete.iam_operate_on_root_account && is_root_account_owns_user) || + (account_to_delete.iam_operate_on_root_account && this._check_root_account(account_data))) { + this._throw_error_delete_conflict(action, account_to_delete, resource_name); + } + return account_data; + } + }); + } + + _check_if_user_does_not_have_access_keys_before_deletion(action, account_to_delete) { + const resource_name = 'access keys'; const is_access_keys_removed = account_to_delete.access_keys.length === 0; if (!is_access_keys_removed) { - dbg.error(`AccountSpaceFS.${action} requested account has access keys`, - account_to_delete); - const message_with_details = `Cannot delete entity, must delete access keys first.`; - const { code, http_code, type } = IamError.DeleteConflict; - throw new IamError({ code, message: message_with_details, http_code, type }); + this._throw_error_delete_conflict(action, account_to_delete, resource_name); } } @@ -710,8 +826,8 @@ class AccountSpaceFS { } _check_specific_access_key_exists(access_keys, access_key_to_find) { - for (const access_key of access_keys) { - if (access_key_to_find === access_key) { + for (const access_key_obj of access_keys) { + if (access_key_to_find === access_key_obj.access_key) { return true; } } @@ -768,14 +884,18 @@ class AccountSpaceFS { } } - async _check_if_account_exists_by_access_key_symlink(action, requesting_account, account_path, access_key) { + async _check_if_account_exists_by_access_key_symlink(action, account_path, access_key_id) { const is_user_account_exists = await native_fs_utils.is_path_exists(this.fs_context, account_path); if (!is_user_account_exists) { - this._throw_access_denied_error(action, requesting_account, { access_key: access_key }, entity_enum.ACCESS_KEY); + dbg.error(`AccountSpaceFS.${action} access key is does not exist`, access_key_id); + const message_with_details = `The Access Key with id ${access_key_id} cannot be found`; + const { code, http_code, type } = IamError.NoSuchEntity; + throw new IamError({ code, message: message_with_details, http_code, type }); } } _check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account) { + if (requesting_account.iam_operate_on_root_account) return; const root_account_id_requesting_account = requesting_account.owner || requesting_account._id; // if it is root account then there is no owner const root_account_id_requested = requested_account.owner || requested_account._id; if (root_account_id_requesting_account !== root_account_id_requested) { @@ -783,6 +903,16 @@ class AccountSpaceFS { } } + _check_access_key_belongs_to_account(action, requested_account, access_key_id) { + const is_access_key_belongs_to_account = this._check_specific_access_key_exists(requested_account.access_keys, access_key_id); + if (!is_access_key_belongs_to_account) { + dbg.error(`AccountSpaceFS.${action} access key is does not exist`, access_key_id); + const message_with_details = `The Access Key with id ${access_key_id} cannot be found`; + const { code, http_code, type } = IamError.NoSuchEntity; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + } + // we will see it after changes in the account (user or access keys) // this change is limited to the specific endpoint that uses _clean_account_cache(requested_account) { diff --git a/src/server/system_services/schemas/nsfs_account_schema.js b/src/server/system_services/schemas/nsfs_account_schema.js index 87b6a297f0..9224c3eb95 100644 --- a/src/server/system_services/schemas/nsfs_account_schema.js +++ b/src/server/system_services/schemas/nsfs_account_schema.js @@ -48,6 +48,11 @@ module.exports = { force_md5_etag: { type: 'boolean', }, + // account with iam_operate_on_root_account property will create root accounts using the IAM API + // (instead of IAM accounts) + iam_operate_on_root_account: { + type: 'boolean' + }, access_keys: { type: 'array', items: { diff --git a/src/test/unit_tests/jest_tests/test_accountspace_fs.test.js b/src/test/unit_tests/jest_tests/test_accountspace_fs.test.js index 915b7cedd3..d93ee0cc51 100644 --- a/src/test/unit_tests/jest_tests/test_accountspace_fs.test.js +++ b/src/test/unit_tests/jest_tests/test_accountspace_fs.test.js @@ -19,6 +19,8 @@ const { IAM_DEFAULT_PATH, access_key_status_enum } = require('../../../endpoint/ const fs_utils = require('../../../util/fs_utils'); const { IamError } = require('../../../endpoint/iam/iam_errors'); const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager').get_instance(); +const native_fs_utils = require('../../../util/native_fs_utils'); +const nsfs_schema_utils = require('../../../manage_nsfs/nsfs_schema_utils'); class NoErrorThrownError extends Error {} @@ -27,6 +29,7 @@ const tmp_fs_path = path.join(TMP_PATH, 'test_accountspace_fs'); const config_root = path.join(tmp_fs_path, 'config_root'); const new_buckets_path1 = path.join(tmp_fs_path, 'new_buckets_path1', '/'); const new_buckets_path2 = path.join(tmp_fs_path, 'new_buckets_path2', '/'); +const new_buckets_path3 = path.join(tmp_fs_path, 'new_buckets_path3', '/'); const accountspace_fs = new AccountSpaceFS({ config_root }); @@ -66,6 +69,25 @@ const root_user_account2 = { master_key_id: '65a62e22ceae5e5f1a758123', }; +const root_user_root_accounts_manager = { + _id: '65a8edc9bc5d5bbf9db71b93', + name: 'test-root-accounts-manager-1003', + email: 'test-root-accounts-manager-1003', + allow_bucket_creation: true, + access_keys: [{ + access_key: 'a-cccdefghijklmn123456', + secret_key: 's-cccdefghijklmn123456EXAMPLE' + }], + nsfs_account_config: { + uid: 1003, + gid: 1003, + new_buckets_path: new_buckets_path3, + }, + creation_date: '2023-10-30T04:46:33.815Z', + master_key_id: '65a62e22ceae5e5f1a758123', + iam_operate_on_root_account: true, +}; + // I'm only interested in the requesting_account field function make_dummy_account_sdk() { return { @@ -93,7 +115,7 @@ function make_dummy_account_sdk_non_root_user() { return account_sdk; } -function make_dummy_account_sdk_iam_user(account, root_account_id) { +function make_dummy_account_sdk_created_from_another_account(account, root_account_id) { return { requesting_account: { _id: account._id, @@ -113,6 +135,12 @@ function make_dummy_account_sdk_iam_user(account, root_account_id) { }; } +function make_dummy_account_sdk_from_root_accounts_manager(account, root_account_manager_id) { + const dummy_account_sdk = make_dummy_account_sdk_created_from_another_account(account, root_account_manager_id); + delete dummy_account_sdk.requesting_account.owner; + return dummy_account_sdk; +} + // use it for root user that doesn't create the resources // (only tries to get, update and delete resources that it doesn't own) function make_dummy_account_sdk_not_for_creating_resources() { @@ -133,6 +161,25 @@ function make_dummy_account_sdk_not_for_creating_resources() { }; } +// I'm only interested in the requesting_account field +function make_dummy_account_sdk_root_accounts_manager() { + return { + requesting_account: { + _id: root_user_root_accounts_manager._id, + name: new SensitiveString(root_user_root_accounts_manager.name), + email: new SensitiveString(root_user_root_accounts_manager.email), + creation_date: root_user_root_accounts_manager.creation_date, + access_keys: [{ + access_key: new SensitiveString(root_user_root_accounts_manager.access_keys[0].access_key), + secret_key: new SensitiveString(root_user_root_accounts_manager.access_keys[0].secret_key) + }], + nsfs_account_config: root_user_root_accounts_manager.nsfs_account_config, + allow_bucket_creation: root_user_root_accounts_manager.allow_bucket_creation, + iam_operate_on_root_account: root_user_root_accounts_manager.iam_operate_on_root_account, + master_key_id: root_user_root_accounts_manager.master_key_id, + }, + }; +} describe('Accountspace_FS tests', () => { @@ -141,9 +188,14 @@ describe('Accountspace_FS tests', () => { await fs_utils.create_fresh_path(accountspace_fs.access_keys_dir); await fs_utils.create_fresh_path(accountspace_fs.buckets_dir); await fs_utils.create_fresh_path(new_buckets_path1); - await fs.promises.chown(new_buckets_path1, root_user_account.nsfs_account_config.uid, root_user_account.nsfs_account_config.gid); + await fs_utils.create_fresh_path(new_buckets_path3); + await fs.promises.chown(new_buckets_path1, + root_user_account.nsfs_account_config.uid, root_user_account.nsfs_account_config.gid); + await fs.promises.chown(new_buckets_path3, + root_user_root_accounts_manager.nsfs_account_config.uid, root_user_root_accounts_manager.nsfs_account_config.gid); - for (const account of [root_user_account, root_user_account2]) { + + for (const account of [root_user_account, root_user_account2, root_user_root_accounts_manager]) { const account_path = accountspace_fs._get_account_config_path(account.name); // assuming that the root account has only 1 access key in the 0 index const account_access_path = accountspace_fs._get_access_keys_config_path(account.access_keys[0].access_key); @@ -164,6 +216,12 @@ describe('Accountspace_FS tests', () => { const dummy_username2 = 'Robert'; const dummy_username3 = 'Alice'; const dummy_username4 = 'James'; + const dummy_username5 = 'Henry'; + const dummy_username6 = 'Mary'; + const dummy_username7 = 'Susan'; + const dummy_username8 = 'Lisa'; + const dummy_username9 = 'Thomas'; + const dummy_username10 = 'Mark'; const dummy_user1 = { username: dummy_username1, iam_path: dummy_iam_path, @@ -172,9 +230,12 @@ describe('Accountspace_FS tests', () => { username: dummy_username2, iam_path: dummy_iam_path, }; + const dummy_user_root_account = { + username: dummy_username5, + }; describe('create_user', () => { - it('create_user should return user params', async function() { + it('create_user should return user params (requesting account is root account to create IAM user)', async function() { const params = { username: dummy_user1.username, iam_path: dummy_user1.iam_path, @@ -194,6 +255,92 @@ describe('Accountspace_FS tests', () => { expect(user_account_config_file.access_keys).toBeDefined(); expect(Array.isArray(user_account_config_file.access_keys)).toBe(true); expect(user_account_config_file.access_keys.length).toBe(0); + expect(user_account_config_file.owner).toBe(account_sdk.requesting_account._id); + expect(user_account_config_file.creator).toBe(account_sdk.requesting_account._id); + }); + + it('create_user should return user params (requesting account is root accounts manager - has allow_bucket_creation true with new_buckets_path - to create root account user)', async function() { + const params = { + username: dummy_user_root_account.username, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const res = await accountspace_fs.create_user(params, account_sdk); + expect(res.iam_path).toBe(IAM_DEFAULT_PATH); + expect(res.username).toBe(params.username); + expect(res.user_id).toBeDefined(); + expect(res.arn).toBeDefined(); + expect(res.create_date).toBeDefined(); + + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + expect(user_account_config_file.name).toBe(params.username); + expect(user_account_config_file._id).toBeDefined(); + expect(user_account_config_file.creation_date).toBeDefined(); + expect(user_account_config_file.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file.access_keys)).toBe(true); + expect(user_account_config_file.access_keys.length).toBe(0); + expect(user_account_config_file.allow_bucket_creation).toBe(true); + expect(user_account_config_file.iam_operate_on_root_account).toBeUndefined(); + expect(user_account_config_file.owner).toBeUndefined(); + expect(user_account_config_file.creator).toBe(account_sdk.requesting_account._id); + }); + + it('create_user should return user params (requesting account is root accounts manager - has allow_bucket_creation false with new_buckets_path - to create root account user)', async function() { + const params = { + username: dummy_username9, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // manipulate the allow_bucket_creation to false + const account_sdk_copy = _.cloneDeep(account_sdk); + account_sdk_copy.requesting_account.allow_bucket_creation = false; + + const res = await accountspace_fs.create_user(params, account_sdk_copy); + expect(res.iam_path).toBe(IAM_DEFAULT_PATH); + expect(res.username).toBe(params.username); + expect(res.user_id).toBeDefined(); + expect(res.arn).toBeDefined(); + expect(res.create_date).toBeDefined(); + + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + expect(user_account_config_file.name).toBe(params.username); + expect(user_account_config_file._id).toBeDefined(); + expect(user_account_config_file.creation_date).toBeDefined(); + expect(user_account_config_file.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file.access_keys)).toBe(true); + expect(user_account_config_file.access_keys.length).toBe(0); + expect(user_account_config_file.allow_bucket_creation).toBe(true); + expect(user_account_config_file.iam_operate_on_root_account).toBeUndefined(); + expect(user_account_config_file.owner).toBeUndefined(); + expect(user_account_config_file.creator).toBe(account_sdk.requesting_account._id); + }); + + it('create_user should return user params (requesting account is root accounts manager - has allow_bucket_creation false without new_buckets_path - to create root account user)', async function() { + const params = { + username: dummy_username10, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // manipulate the allow_bucket_creation to false, remove new_buckets_path + const account_sdk_copy = _.cloneDeep(account_sdk); + account_sdk_copy.requesting_account.allow_bucket_creation = false; + delete account_sdk_copy.requesting_account.nsfs_account_config.new_buckets_path; + + const res = await accountspace_fs.create_user(params, account_sdk_copy); + expect(res.iam_path).toBe(IAM_DEFAULT_PATH); + expect(res.username).toBe(params.username); + expect(res.user_id).toBeDefined(); + expect(res.arn).toBeDefined(); + expect(res.create_date).toBeDefined(); + + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + expect(user_account_config_file.name).toBe(params.username); + expect(user_account_config_file._id).toBeDefined(); + expect(user_account_config_file.creation_date).toBeDefined(); + expect(user_account_config_file.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file.access_keys)).toBe(true); + expect(user_account_config_file.access_keys.length).toBe(0); + expect(user_account_config_file.allow_bucket_creation).toBe(false); + expect(user_account_config_file.iam_operate_on_root_account).toBeUndefined(); + expect(user_account_config_file.owner).toBeUndefined(); + expect(user_account_config_file.creator).toBe(account_sdk.requesting_account._id); }); it('create_user should return an error if requesting user is not a root account user', async function() { @@ -230,7 +377,7 @@ describe('Accountspace_FS tests', () => { }); describe('get_user', () => { - it('get_user should return user params', async function() { + it('get_user should return user params (requesting account is root account to create IAM user)', async function() { const params = { username: dummy_user1.username, }; @@ -244,6 +391,20 @@ describe('Accountspace_FS tests', () => { expect(res.password_last_used).toBeDefined(); }); + it('get_user should return user params (requesting account is root accounts manager to create root account user)', async function() { + const params = { + username: dummy_user_root_account.username, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const res = await accountspace_fs.get_user(params, account_sdk); + expect(res.user_id).toBeDefined(); + expect(res.iam_path).toBe(IAM_DEFAULT_PATH); + expect(res.username).toBe(dummy_user_root_account.username); + expect(res.arn).toBeDefined(); + expect(res.create_date).toBeDefined(); + expect(res.password_last_used).toBeDefined(); + }); + it('get_user should return an error if requesting user is not a root account user', async function() { try { const params = { @@ -261,7 +422,7 @@ describe('Accountspace_FS tests', () => { it('get_user should return an error if user to get is a root account user', async function() { try { const params = { - username: root_user_account.name, + username: root_user_root_accounts_manager.name, }; const account_sdk = make_dummy_account_sdk(); await accountspace_fs.get_user(params, account_sdk); @@ -343,7 +504,7 @@ describe('Accountspace_FS tests', () => { expect(user_account_config_file.iam_path).toBe(dummy_user1.iam_path); }); - it('update_user with new_iam_path should return user params and update the iam_path', async function() { + it('update_user with new_iam_path should return user params and update the iam_path (requesting account is root account to create IAM user)', async function() { let params = { username: dummy_user1.username, new_iam_path: dummy_iam_path2, @@ -365,6 +526,22 @@ describe('Accountspace_FS tests', () => { await accountspace_fs.update_user(params, account_sdk); }); + it('update_user with new_iam_path should return user params and update the iam_path (requesting account is root accounts manager to create root account user)', async function() { + const params = { + username: dummy_user_root_account.username, + new_iam_path: dummy_iam_path, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const res = await accountspace_fs.update_user(params, account_sdk); + expect(res.iam_path).toBe(dummy_iam_path); + expect(res.username).toBe(dummy_user_root_account.username); + expect(res.user_id).toBeDefined(); + expect(res.arn).toBeDefined(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + expect(user_account_config_file.name).toBe(params.username); + expect(user_account_config_file.iam_path).toBe(dummy_iam_path); + }); + it('update_user should return an error if requesting user is not a root account user', async function() { try { const params = { @@ -382,7 +559,7 @@ describe('Accountspace_FS tests', () => { it('update_user should return an error if user to update is a root account user', async function() { try { const params = { - username: root_user_account.name, + username: root_user_root_accounts_manager.name, }; const account_sdk = make_dummy_account_sdk(); await accountspace_fs.update_user(params, account_sdk); @@ -498,7 +675,7 @@ describe('Accountspace_FS tests', () => { }); describe('delete_user', () => { - it('delete_user does not return any params', async function() { + it('delete_user does not return any params (requesting account is root account to create IAM user)', async function() { const params = { username: dummy_user1.username, }; @@ -509,6 +686,17 @@ describe('Accountspace_FS tests', () => { await fs_utils.file_must_not_exist(user_account_config_path); }); + it('delete_user does not return any params (requesting account is root accounts manager to create root account user)', async function() { + const params = { + username: dummy_user_root_account.username, + }; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const res = await accountspace_fs.delete_user(params, account_sdk); + expect(res).toBeUndefined(); + const user_account_config_path = path.join(accountspace_fs.accounts_dir, params.username + '.json'); + await fs_utils.file_must_not_exist(user_account_config_path); + }); + it('delete_user should return an error if requesting user is not a root account user', async function() { try { const params = { @@ -526,7 +714,7 @@ describe('Accountspace_FS tests', () => { it('delete_user should return an error if user to delete is a root account user', async function() { try { const params = { - username: root_user_account.name, + username: root_user_root_accounts_manager.name, }; const account_sdk = make_dummy_account_sdk(); await accountspace_fs.delete_user(params, account_sdk); @@ -579,6 +767,69 @@ describe('Accountspace_FS tests', () => { } catch (err) { expect(err).toBeInstanceOf(IamError); expect(err).toHaveProperty('code', IamError.DeleteConflict.code); + expect(err).toHaveProperty('message'); + expect(err.message).toMatch(/must delete access keys first/i); + const user_account_config_path = path.join(accountspace_fs.accounts_dir, params.username + '.json'); + await fs_utils.file_must_exist(user_account_config_path); + } + }); + + it('delete_user should return an error if user has IAM users', async function() { + const username_for_root_account = dummy_username6; + const params = { + username: username_for_root_account, + }; + try { + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // create the root account + await accountspace_fs.create_user(params, account_sdk); + // create the root account access key + // same params + await accountspace_fs.create_access_key(params, account_sdk); + // create a user with the root account + const account_config_file = await read_config_file(accountspace_fs.accounts_dir, username_for_root_account); + const root_account_manager_id = account_sdk.requesting_account._id; + const account_sdk_root = make_dummy_account_sdk_from_root_accounts_manager( + account_config_file, root_account_manager_id); + const username = dummy_username7; + const params_for_iam_user_creation = { + username: username, + }; + await accountspace_fs.create_user(params_for_iam_user_creation, account_sdk_root); + // delete the created root account + // same params + await accountspace_fs.delete_user(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.DeleteConflict.code); + expect(err).toHaveProperty('message'); + expect(err.message).toMatch(/must delete IAM users first/i); + const user_account_config_path = path.join(accountspace_fs.accounts_dir, params.username + '.json'); + await fs_utils.file_must_exist(user_account_config_path); + } + }); + + it('delete_user should return an error if user has buckets', async function() { + const username_for_root_account = dummy_username8; + const params = { + username: username_for_root_account, + }; + try { + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // create the root account + await accountspace_fs.create_user(params, account_sdk); + // create a dummy bucket + const bucket_name = `my-bucket-${params.username}`; + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + await create_dummy_bucket(user_account_config_file, bucket_name); + await accountspace_fs.delete_user(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.DeleteConflict.code); + expect(err).toHaveProperty('message'); + expect(err.message).toMatch(/must delete buckets first/i); const user_account_config_path = path.join(accountspace_fs.accounts_dir, params.username + '.json'); await fs_utils.file_must_exist(user_account_config_path); } @@ -586,7 +837,7 @@ describe('Accountspace_FS tests', () => { }); describe('list_users', () => { - it('list_users return array of users and value of is_truncated', async function() { + it('list_users return array of users and value of is_truncated (requesting account is root account to create IAM user)', async function() { const params = {}; const account_sdk = make_dummy_account_sdk(); const res = await accountspace_fs.list_users(params, account_sdk); @@ -595,6 +846,15 @@ describe('Accountspace_FS tests', () => { expect(typeof res.is_truncated === 'boolean').toBe(true); }); + it('list_users return array of users and value of is_truncated (requesting account is root accounts manager to create root account user)', async function() { + const params = {}; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const res = await accountspace_fs.list_users(params, account_sdk); + expect(Array.isArray(res.members)).toBe(true); + expect(res.members.length).toBeGreaterThan(1); // will always have at least 1 account (himself) + expect(typeof res.is_truncated === 'boolean').toBe(true); + }); + it('list_users return an empty array of users and value of is_truncated if non of the users has the iam_path_prefix', async function() { const params = { iam_path_prefix: 'non_existing_division/non-existing_subdivision' @@ -653,6 +913,7 @@ describe('Accountspace_FS tests', () => { const dummy_username4 = 'James'; const dummy_username5 = 'Oliver'; const dummy_username6 = 'Henry'; + const dummy_username7 = 'Noah'; const dummy_user1 = { username: dummy_username1, path: dummy_path, @@ -665,15 +926,19 @@ describe('Accountspace_FS tests', () => { username: dummy_username3, path: dummy_path, }; + const dummy_user_root_account = { + username: dummy_username7, + }; + beforeAll(async () => { await fs_utils.create_fresh_path(accountspace_fs.accounts_dir); await fs_utils.create_fresh_path(accountspace_fs.access_keys_dir); await fs_utils.create_fresh_path(accountspace_fs.buckets_dir); await fs_utils.create_fresh_path(new_buckets_path1); await fs.promises.chown(new_buckets_path1, - root_user_account.nsfs_account_config.uid, root_user_account.nsfs_account_config.gid); + root_user_root_accounts_manager.nsfs_account_config.uid, root_user_root_accounts_manager.nsfs_account_config.gid); - for (const account of [root_user_account]) { + for (const account of [root_user_root_accounts_manager]) { const account_path = accountspace_fs._get_account_config_path(account.name); // assuming that the root account has only 1 access key in the 0 index const account_access_path = accountspace_fs._get_access_keys_config_path(account.access_keys[0].access_key); @@ -820,7 +1085,8 @@ describe('Accountspace_FS tests', () => { let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); // create the second access key // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, account_sdk.requesting_account._id); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, + account_sdk.requesting_account._id); const params = {}; const res = await accountspace_fs.create_access_key(params, account_sdk); expect(res.username).toBe(dummy_username5); @@ -849,7 +1115,8 @@ describe('Accountspace_FS tests', () => { const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); // create the second access key // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, account_sdk.requesting_account._id); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, + account_sdk.requesting_account._id); const params = { username: dummy_user1.username, }; @@ -860,11 +1127,55 @@ describe('Accountspace_FS tests', () => { expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); + + it('create_access_key should return user access key params (requesting account is root accounts manager to create access keys for root account user)', async function() { + const username = dummy_user_root_account.username; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // create the user + const params = { + username: username, + }; + await accountspace_fs.create_user(params, account_sdk); + // create the access key + const res = await accountspace_fs.create_access_key(params, account_sdk); + expect(res.username).toBe(username); + expect(res.access_key).toBeDefined(); + expect(res.status).toBe('Active'); + expect(res.secret_key).toBeDefined(); + + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.username); + expect(user_account_config_file.name).toBe(params.username); + expect(user_account_config_file.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file.access_keys)).toBe(true); + expect(user_account_config_file.access_keys.length).toBe(1); + + const access_key = res.access_key; + const user_account_config_file_from_symlink = await read_config_file(accountspace_fs.access_keys_dir, access_key, true); + expect(user_account_config_file_from_symlink.name).toBe(params.username); + expect(user_account_config_file_from_symlink.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file_from_symlink.access_keys)).toBe(true); + }); + + it('create_access_key should return an error if user is IAM user (requesting account is root accounts manager)', async function() { + try { + // user already created (IAM user) + // create the access key + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const params = { + username: dummy_username1, + }; + await accountspace_fs.create_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NotAuthorized.code); + } + }); }); describe('get_access_key_last_used', () => { const dummy_region = 'us-west-2'; - it('get_access_key_last_used should return user access key params', async function() { + it('get_access_key_last_used should return user access key params (requesting account is root account to create IAM user)', async function() { const account_sdk = make_dummy_account_sdk(); // create the user const params_for_user_creation = { @@ -898,9 +1209,10 @@ describe('Accountspace_FS tests', () => { access_key: dummy_access_key, }; await accountspace_fs.get_access_key_last_used(params, account_sdk); + throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); @@ -925,6 +1237,7 @@ describe('Accountspace_FS tests', () => { access_key: dummy_access_key, }; await accountspace_fs.get_access_key_last_used(params, account_sdk2); + throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); @@ -932,11 +1245,12 @@ describe('Accountspace_FS tests', () => { }); it('get_access_key_last_used should return user access key params (requester is an IAM user)', async function() { + const username = dummy_user2.username; let account_sdk = make_dummy_account_sdk(); - const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, user_account_config_file.owner); - const access_key = user_account_config_file.access_keys[1].access_key; + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, user_account_config_file.owner); + const access_key = user_account_config_file.access_keys[0].access_key; const params = { access_key: access_key, }; @@ -944,11 +1258,49 @@ describe('Accountspace_FS tests', () => { expect(res.region).toBe(dummy_region); expect(res).toHaveProperty('last_used_date'); expect(res).toHaveProperty('service_name'); - expect(res.username).toBe(user_account_config_file.name); + expect(res.username).toBe(username); }); - // I didn't add here a test of 'get_access_key_last_used return an error if user is not owned by the root account (requester is an IAM user)' - // because UserName is not passed in this API call + it('get_access_key_last_used return an error if user is not owned by the root account (requester is an IAM user)', async function() { + try { + let account_sdk = make_dummy_account_sdk(); + const requester_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_user2.username); + // by the IAM user + account_sdk = make_dummy_account_sdk_created_from_another_account(requester_account_config_file, + requester_account_config_file.owner); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_user_root_account.username); + const access_key = user_account_config_file.access_keys[0].access_key; + const params = { + access_key: access_key, + }; + await accountspace_fs.get_access_key_last_used(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('get_access_key_last_used should return user access key params (requesting account is root accounts manager requested account is root account)', async function() { + const username = dummy_user_root_account.username; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + // user was already created + // create the access key + const params_for_access_key_creation = { + username: username, + }; + const res_access_key_created = await accountspace_fs.create_access_key(params_for_access_key_creation, account_sdk); + const dummy_access_key = res_access_key_created.access_key; + // get the access key + const params = { + access_key: dummy_access_key, + }; + const res = await accountspace_fs.get_access_key_last_used(params, account_sdk); + expect(res.region).toBe(dummy_region); + expect(res).toHaveProperty('last_used_date'); + expect(res).toHaveProperty('service_name'); + expect(res.username).toBe(username); + }); }); describe('update_access_key', () => { @@ -982,7 +1334,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); @@ -1004,7 +1356,7 @@ describe('Accountspace_FS tests', () => { } }); - it('update_access_key should not return any param (update status to Inactive)', async function() { + it('update_access_key should not return any param (update status to Inactive) (requesting account is root account to create IAM user)', async function() { const account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username1); const access_key = user_account_config_file.access_keys[0].access_key; @@ -1019,7 +1371,7 @@ describe('Accountspace_FS tests', () => { expect(user_account_config_file.access_keys[0].deactivated).toBe(true); }); - it('update_access_key should not return any param (update status to Active)', async function() { + it('update_access_key should not return any param (update status to Active) (requesting account is root account to create IAM user)', async function() { const account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username1); const access_key = user_account_config_file.access_keys[0].access_key; @@ -1034,7 +1386,7 @@ describe('Accountspace_FS tests', () => { expect(user_account_config_file.access_keys[0].deactivated).toBe(false); }); - it('update_access_key should not return any param (update status to Active, already was Active)', async function() { + it('update_access_key should not return any param (update status to Active, already was Active) (requesting account is root account to create IAM user)', async function() { const account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username1); const access_key = user_account_config_file.access_keys[0].access_key; @@ -1054,7 +1406,7 @@ describe('Accountspace_FS tests', () => { let account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username); // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, user_account_config_file.owner); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, user_account_config_file.owner); const access_key = user_account_config_file.access_keys[1].access_key; const params = { access_key: access_key, @@ -1074,7 +1426,8 @@ describe('Accountspace_FS tests', () => { const access_key = user_account_config_file.access_keys[0].access_key; // create the second access key // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, account_sdk.requesting_account._id); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, + account_sdk.requesting_account._id); const params = { username: dummy_user1.username, access_key: access_key, @@ -1087,6 +1440,22 @@ describe('Accountspace_FS tests', () => { expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); + + it('update_access_key should not return any param (update status to Inactive) (requesting account is root accounts manager requested account is root account)', async function() { + const username = dummy_user_root_account.username; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); + const access_key = user_account_config_file.access_keys[0].access_key; + const params = { + username: username, + access_key: access_key, + status: access_key_status_enum.INACTIVE, + }; + const res = await accountspace_fs.update_access_key(params, account_sdk); + expect(res).toBeUndefined(); + user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); + expect(user_account_config_file.access_keys[0].deactivated).toBe(true); + }); }); describe('delete_access_key', () => { @@ -1118,7 +1487,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); @@ -1139,7 +1508,7 @@ describe('Accountspace_FS tests', () => { } }); - it('delete_access_key should not return any param', async function() { + it('delete_access_key should not return any param (requesting account is root account to create IAM user)', async function() { const account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username1); const access_key = user_account_config_file.access_keys[0].access_key; @@ -1155,7 +1524,7 @@ describe('Accountspace_FS tests', () => { await fs_utils.file_must_not_exist(symlink_config_path); }); - it('delete_access_key should not return any param (account with 2 access keys)', async function() { + it('delete_access_key should not return any param (account with 2 access keys) (requesting account is root account to create IAM user)', async function() { const username = dummy_username6; const account_sdk = make_dummy_account_sdk(); // create the user @@ -1193,7 +1562,7 @@ describe('Accountspace_FS tests', () => { let account_sdk = make_dummy_account_sdk(); let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, user_account_config_file.owner); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, user_account_config_file.owner); const access_key = user_account_config_file.access_keys[1].access_key; const params = { access_key: access_key, @@ -1215,7 +1584,8 @@ describe('Accountspace_FS tests', () => { const access_key = user_account_config_file.access_keys[0].access_key; // create the second access key // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, account_sdk.requesting_account._id); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, + account_sdk.requesting_account._id); const params = { username: dummy_user1.username, access_key: access_key, @@ -1227,10 +1597,27 @@ describe('Accountspace_FS tests', () => { expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); + + it('delete_access_key should not return any param (requesting account is root accounts manager requested account is root account)', async function() { + const username = dummy_user_root_account.username; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + let user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); + const access_key = user_account_config_file.access_keys[0].access_key; + const params = { + username: username, + access_key: access_key, + }; + const res = await accountspace_fs.delete_access_key(params, account_sdk); + expect(res).toBeUndefined(); + user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); + expect(user_account_config_file.access_keys.length).toBe(1); + const symlink_config_path = path.join(accountspace_fs.access_keys_dir, access_key + '.symlink'); + await fs_utils.file_must_not_exist(symlink_config_path); + }); }); describe('list_access_keys', () => { - it('list_access_keys return array of access_keys and value of is_truncated', async function() { + it('list_access_keys return array of access_keys and value of is_truncated (requesting account is root account and requested account is IAM user)', async function() { const params = { username: dummy_username1, }; @@ -1295,7 +1682,7 @@ describe('Accountspace_FS tests', () => { let account_sdk = make_dummy_account_sdk(); const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, user_account_config_file.owner); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, user_account_config_file.owner); const params = {}; const res = await accountspace_fs.list_access_keys(params, account_sdk); expect(Array.isArray(res.members)).toBe(true); @@ -1311,7 +1698,8 @@ describe('Accountspace_FS tests', () => { const access_key = user_account_config_file.access_keys[0].access_key; // create the second access key // by the IAM user - account_sdk = make_dummy_account_sdk_iam_user(user_account_config_file, account_sdk.requesting_account._id); + account_sdk = make_dummy_account_sdk_created_from_another_account(user_account_config_file, + account_sdk.requesting_account._id); const params = { username: dummy_user1.username, access_key: access_key, @@ -1323,6 +1711,22 @@ describe('Accountspace_FS tests', () => { expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); + + it('list_access_keys return array of access_keys and value of is_truncated (requesting account is root accounts manager requested account is root account)', async function() { + const username = dummy_user_root_account.username; + const account_sdk = make_dummy_account_sdk_root_accounts_manager(); + const params = { + username: username, + }; + const res = await accountspace_fs.list_access_keys(params, account_sdk); + expect(Array.isArray(res.members)).toBe(true); + expect(typeof res.is_truncated === 'boolean').toBe(true); + expect(res.members.length).toBe(1); + expect(res.members[0]).toHaveProperty('username', username); + expect(res.members[0].access_key).toBeDefined(); + expect(res.members[0].status).toBeDefined(); + expect(res.members[0].create_date).toBeDefined(); + }); }); }); }); @@ -1346,3 +1750,29 @@ async function read_config_file(account_path, config_file_name, is_symlink) { } return config_data; } + +async function create_dummy_bucket(account, bucket_name) { + const bucket_storage_path = path.join(account.nsfs_account_config.new_buckets_path, bucket_name); + const bucket = _new_bucket_defaults(account, bucket_name, bucket_storage_path); + const bucket_config = JSON.stringify(bucket); + const bucket_to_validate = JSON.parse(bucket_config); + nsfs_schema_utils.validate_bucket_schema(bucket_to_validate); + const bucket_config_path = path.join(accountspace_fs.buckets_dir, bucket_name + '.json'); + await native_fs_utils.create_config_file(accountspace_fs.fs_context, accountspace_fs.buckets_dir, bucket_config_path, bucket_config); + return bucket_config_path; +} + +// parital copy from bucketspace_fs +function _new_bucket_defaults(account, bucket_name, bucket_storage_path) { + return { + _id: '65a8edc9bc5d5bbf9db71c75', + name: bucket_name, + owner_account: account._id, + system_owner: new SensitiveString(account.name), + bucket_owner: new SensitiveString(account.name), + creation_date: new Date().toISOString(), + path: bucket_storage_path, + should_create_underlying_storage: true, + versioning: 'DISABLED', + }; +} diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js index a46c011e76..085bd200ac 100644 --- a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js @@ -14,7 +14,7 @@ const fs_utils = require('../../../util/fs_utils'); const nb_native = require('../../../util/nb_native'); const { set_path_permissions_and_owner, create_fs_user_by_platform, delete_fs_user_by_platform, TMP_PATH, set_nc_config_dir_in_config } = require('../../system_tests/test_utils'); -const { get_process_fs_context } = require('../../../util/native_fs_utils'); +const { get_process_fs_context, update_config_file } = require('../../../util/native_fs_utils'); const { TYPES, ACTIONS, CONFIG_SUBDIRS, ANONYMOUS } = require('../../../manage_nsfs/manage_nsfs_constants'); const ManageCLIError = require('../../../manage_nsfs/manage_nsfs_cli_errors').ManageCLIError; const ManageCLIResponse = require('../../../manage_nsfs/manage_nsfs_cli_responses').ManageCLIResponse; @@ -379,6 +379,51 @@ describe('manage nsfs cli account flow', () => { expect(account.force_md5_etag).toBe(true); }); + it('cli account add - use flag iam_operate_on_root_account (true)', async function() { + const action = ACTIONS.ADD; + const { type, name, new_buckets_path, uid, gid } = defaults; + const iam_operate_on_root_account = 'true'; + const account_options = { config_root, name, new_buckets_path, uid, gid, + iam_operate_on_root_account }; + await fs_utils.create_fresh_path(new_buckets_path); + await fs_utils.file_must_exist(new_buckets_path); + await set_path_permissions_and_owner(new_buckets_path, account_options, 0o700); + await exec_manage_cli(type, action, account_options); + const account = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + expect(account.iam_operate_on_root_account).toBe(true); + expect(account.allow_bucket_creation).toBe(true); + }); + + it('cli account add - use flag iam_operate_on_root_account (false)', async function() { + const action = ACTIONS.ADD; + const { type, name, new_buckets_path, uid, gid } = defaults; + const iam_operate_on_root_account = 'false'; + const account_options = { config_root, name, new_buckets_path, uid, gid, + iam_operate_on_root_account }; + await fs_utils.create_fresh_path(new_buckets_path); + await fs_utils.file_must_exist(new_buckets_path); + await set_path_permissions_and_owner(new_buckets_path, account_options, 0o700); + await exec_manage_cli(type, action, account_options); + const account = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + expect(account.iam_operate_on_root_account).toBe(false); + expect(account.allow_bucket_creation).toBe(true); // by default it is inferred when we have new_buckets_path + }); + + it('cli account add - use flag iam_operate_on_root_account (true) ' + + 'with allow_bucket_creation (true)', async function() { + const action = ACTIONS.ADD; + const { type, name, new_buckets_path, uid, gid } = defaults; + const iam_operate_on_root_account = 'true'; + const allow_bucket_creation = 'true'; // root accounts manager is not allowed to create buckets + const account_options = { config_root, name, new_buckets_path, uid, gid, + iam_operate_on_root_account, allow_bucket_creation }; + await fs_utils.create_fresh_path(new_buckets_path); + await fs_utils.file_must_exist(new_buckets_path); + await set_path_permissions_and_owner(new_buckets_path, account_options, 0o700); + const res = await exec_manage_cli(type, action, account_options); + expect(JSON.parse(res).response.code).toEqual(ManageCLIResponse.AccountCreated.code); + }); + it('should fail - cli account add invalid flags combination (gid and user)', async function() { const action = ACTIONS.ADD; const { type, name, gid } = defaults; @@ -733,6 +778,100 @@ describe('manage nsfs cli account flow', () => { expect(new_account_details.force_md5_etag).toBeUndefined(); }); + it('cli update account set flag iam_operate_on_root_account', async function() { + const { name } = defaults; + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + const action = ACTIONS.UPDATE; + await exec_manage_cli(type, action, account_options); + let new_account_details = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + expect(new_account_details.iam_operate_on_root_account).toBe(true); + + account_options.iam_operate_on_root_account = 'false'; + await exec_manage_cli(type, action, account_options); + new_account_details = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + expect(new_account_details.iam_operate_on_root_account).toBe(false); + }); + + it(`should fail - cli update account unset flag iam_operate_on_root_account with ''`, async function() { + // first set the value of iam_operate_on_root_account to be true + const { name } = defaults; + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + const action = ACTIONS.UPDATE; + await exec_manage_cli(type, action, account_options); + const new_account_details = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + expect(new_account_details.iam_operate_on_root_account).toBe(true); + + // unset iam_operate_on_root_account (is not allowed) + const empty_string = '\'\''; + account_options.iam_operate_on_root_account = empty_string; + const res = await exec_manage_cli(type, action, account_options); + expect(JSON.parse(res.stdout).error.code).toBe( + ManageCLIError.InvalidArgumentType.code); + }); + + it('cli update account iam_operate_on_root_account true when account owns a bucket', async function() { + // cli create bucket + const bucket_name = 'my-bucket'; + let action = ACTIONS.ADD; + const { new_buckets_path } = defaults; + const account_name = defaults.name; + const bucket_options = { config_root, path: new_buckets_path, name: bucket_name, owner: account_name}; + await exec_manage_cli(TYPES.BUCKET, action, bucket_options); + + // set the value of iam_operate_on_root_account to be true + const { name } = defaults; + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + action = ACTIONS.UPDATE; + const res = await exec_manage_cli(type, action, account_options); + expect(JSON.parse(res).response.code).toEqual(ManageCLIResponse.AccountUpdated.code); + }); + + // TODO (after we add the ability to create IAM accounts using the noobaa cli) + it('should fail - cli update account iam_operate_on_root_account true when account owns IAM accounts', async function() { + const { name } = defaults; + const accounts_details = await read_config_file(config_root, CONFIG_SUBDIRS.ACCOUNTS, name); + const account_id = accounts_details._id; + + expect(true).toBe(true); + const account_name = 'account-to-be-owned'; + const account_options1 = { config_root, name: account_name, uid: 5555, gid: 5555 }; + await exec_manage_cli(type, ACTIONS.ADD, account_options1); + + // update the account to have the property owner + // (we use this way because now we don't have the way to create IAM users through the noobaa cli) + const account_config_path = path.join(config_root, CONFIG_SUBDIRS.ACCOUNTS, account_name + '.json'); + const { data } = await nb_native().fs.readFile(DEFAULT_FS_CONFIG, account_config_path); + const config_data = JSON.parse(data.toString()); + config_data.owner = account_id; // just so we can identify this account as IAM user + await update_config_file(DEFAULT_FS_CONFIG, CONFIG_SUBDIRS.ACCOUNTS, + account_config_path, JSON.stringify(config_data)); + + // set the value of iam_operate_on_root_account to be true + const account_options2 = { config_root, name, iam_operate_on_root_account: true}; + const res = await exec_manage_cli(type, ACTIONS.UPDATE, account_options2); + expect(JSON.parse(res.stdout).error.code).toBe( + ManageCLIError.AccountCannotBeRootAccountsManager.code); + }); + + it('should fail - cli update account iam_operate_on_root_account true when requester is IAM user', async function() { + // update the account to have the property owner + // (we use this way because now we don't have the way to create IAM users through the noobaa cli) + const { name } = defaults; + const account_config_path = path.join(config_root, CONFIG_SUBDIRS.ACCOUNTS, name + '.json'); + const { data } = await nb_native().fs.readFile(DEFAULT_FS_CONFIG, account_config_path); + const config_data = JSON.parse(data.toString()); + config_data.owner = '65a62e22ceae5e5f1a758aa9'; // just so we can identify this account as IAM user + await update_config_file(DEFAULT_FS_CONFIG, CONFIG_SUBDIRS.ACCOUNTS, + account_config_path, JSON.stringify(config_data)); + + // set the value of iam_operate_on_root_account to be true + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + const action = ACTIONS.UPDATE; + const res = await exec_manage_cli(type, action, account_options); + expect(JSON.parse(res.stdout).error.code).toBe( + ManageCLIError.AccountCannotCreateRootAccountsRequesterIAMUser.code); + }); + it('should fail - cli update account without a property to update', async () => { const action = ACTIONS.UPDATE; const { name } = defaults; diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js index 01da4d3ba6..2327b2fdf5 100644 --- a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js @@ -75,6 +75,12 @@ describe('schema validation NC NSFS account', () => { nsfs_schema_utils.validate_account_schema(account_data); }); + it('account with iam_operate_on_root_account', () => { + const account_data = get_account_data(); + account_data.iam_operate_on_root_account = true; + nsfs_schema_utils.validate_account_schema(account_data); + }); + it('account with 2 access_keys objects (with additional properties) in the access_key array', () => { const account_data = get_account_data(); account_data.access_keys[1] = get_access_key_with_additional_properties(); @@ -428,6 +434,15 @@ describe('schema validation NC NSFS account', () => { assert_validation(account_data, reason, message); }); + it('account with iam_operate_on_root_account as a number (instead of boolean)', () => { + const account_data = get_account_data(); + account_data.iam_operate_on_root_account = 123; + const reason = 'Test should have failed because of wrong type for' + + 'iam_operate_on_root_account with number (instead of boolean)'; + const message = 'must be boolean'; + assert_validation(account_data, reason, message); + }); + it('account with access_key array with index 0 undefined', () => { const account_data = get_account_data(); account_data.access_keys[1] = get_access_key_with_additional_properties(); diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_cli.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_cli.test.js index cff13bdb02..0213fdb4e5 100644 --- a/src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_cli.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_cli.test.js @@ -143,6 +143,23 @@ describe('manage nsfs cli bucket flow', () => { expect(JSON.parse(res).response.code).toEqual(ManageCLIResponse.BucketCreated.code); }); + it('cli create bucket - owner has iam_operate_on_root_account true', async () => { + // update the account to have iam_operate_on_root_account true + const { name } = account_defaults; + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + let action = ACTIONS.UPDATE; + await exec_manage_cli(TYPES.ACCOUNT, action, account_options); + + // create the bucket + action = ACTIONS.ADD; + const bucket_options = { config_root, ...bucket_defaults}; + await fs_utils.create_fresh_path(bucket_options.path); + await fs_utils.file_must_exist(bucket_options.path); + await set_path_permissions_and_owner(bucket_options.path, account_defaults, 0o700); + const res = await exec_manage_cli(TYPES.BUCKET, action, bucket_options); + expect(JSON.parse(res).response.code).toEqual(ManageCLIResponse.BucketCreated.code); + }); + it('should fail - cli bucket add - without identifier', async () => { const action = ACTIONS.ADD; const bucket_options = { config_root, owner: bucket_defaults.owner, path: bucket_defaults.path }; @@ -473,6 +490,22 @@ describe('manage nsfs cli bucket flow', () => { const res = await exec_manage_cli(TYPES.BUCKET, action, bucket_options); expect(JSON.parse(res.stdout).error.code).toBe(ManageCLIError.MissingBucketNameFlag.code); }); + + it('cli update bucket owner - owner has iam_operate_on_root_account true', async () => { + // update the account to have iam_operate_on_root_account true + const { name } = account_defaults2; + const account_options = { config_root, name, iam_operate_on_root_account: 'true'}; + let action = ACTIONS.UPDATE; + await exec_manage_cli(TYPES.ACCOUNT, action, account_options); + + action = ACTIONS.UPDATE; + const bucket_options = { config_root, name: bucket_defaults.name, owner: account_defaults2.name}; + await fs_utils.create_fresh_path(bucket_defaults.path); + await fs_utils.file_must_exist(bucket_defaults.path); + await set_path_permissions_and_owner(bucket_defaults.path, account_defaults2, 0o700); + const res = await exec_manage_cli(TYPES.BUCKET, action, bucket_options); + expect(JSON.parse(res).response.code).toEqual(ManageCLIResponse.BucketUpdated.code); + }); }); describe('cli delete bucket', () => {