diff --git a/src/endpoint/iam/iam_rest.js b/src/endpoint/iam/iam_rest.js index a0262e8d99..58d4f9d5a5 100644 --- a/src/endpoint/iam/iam_rest.js +++ b/src/endpoint/iam/iam_rest.js @@ -19,7 +19,8 @@ const RPC_ERRORS_TO_IAM = Object.freeze({ INVALID_ACCESS_KEY_ID: IamError.InvalidClientTokenId, DEACTIVATED_ACCESS_KEY_ID: IamError.InvalidClientTokenIdInactiveAccessKey, NO_SUCH_ACCOUNT: IamError.AccessDeniedException, - NO_SUCH_ROLE: IamError.AccessDeniedException + NO_SUCH_ROLE: IamError.AccessDeniedException, + VALIDATION_ERROR: IamError.ValidationError, }); const ACTIONS = Object.freeze({ diff --git a/src/endpoint/iam/iam_utils.js b/src/endpoint/iam/iam_utils.js index 658f4dd75d..ac9ce55ac9 100644 --- a/src/endpoint/iam/iam_utils.js +++ b/src/endpoint/iam/iam_utils.js @@ -4,9 +4,10 @@ const _ = require('lodash'); const s3_utils = require('../s3/s3_utils'); const { IamError } = require('./iam_errors'); -const { AWS_IAM_PATH_REGEXP, AWS_USERNAME_REGEXP, AWS_IAM_LIST_MARKER, AWS_IAM_ACCESS_KEY_INPUT_REGEXP } = require('../../util/string_utils'); +const { AWS_IAM_PATH_REGEXP, AWS_IAM_LIST_MARKER, AWS_IAM_ACCESS_KEY_INPUT_REGEXP } = require('../../util/string_utils'); const iam_constants = require('./iam_constants'); const { RpcError } = require('../../rpc'); +const validation_utils = require('../../util/validation_utils'); /** * format_iam_xml_date return the date without milliseconds @@ -79,67 +80,6 @@ function parse_max_items(input_max_items) { return value_as_number; } - -/** - * _type_check_input checks that the input is the same as needed - * @param {string} input_type - * @param {string | number} input_value - * @param {string} parameter_name - */ -function _type_check_input(input_type, input_value, parameter_name) { - if (typeof input_value !== input_type) { - const message_with_details = `1 validation error detected: Value ${input_value} at ` + - `'${parameter_name}' failed to satisfy constraint: Member must be ${input_type}`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } -} - -/** - * _length_min_check_input checks if the input is lower than the min length - * @param {number} min_length - * @param {any} input_value - * @param {string} parameter_name - */ -function _length_min_check_input(min_length, input_value, parameter_name) { - const input_length = input_value.length; - if (input_length < min_length) { - const message_with_details = `Invalid length for parameter ${parameter_name}, ` + - `value: ${input_length}, valid min length: ${min_length}`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } -} - -/** - * _length_max_check_input checks if the input is higher than the max length - * @param {number} max_length - * @param {any} input_value - * @param {string} parameter_name - */ -function _length_max_check_input(max_length, input_value, parameter_name) { - const input_length = input_value.length; - if (input_length > max_length) { - const message_with_details = `1 validation error detected: Value ${input_value} at ` + - `'${parameter_name}' failed to satisfy constraint:` + - `Member must have length less than or equal to ${max_length}`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } -} - -/** - * _length_check_input checks that the input length is between the min and the max value - * @param {number} min_length - * @param {number} max_length - * @param {string} input_value - * @param {string} parameter_name - */ -function _length_check_input(min_length, max_length, input_value, parameter_name) { - _length_min_check_input(min_length, input_value, parameter_name); - _length_max_check_input(max_length, input_value, parameter_name); -} - /** * validate_params will call the aquivalent function in user or access key * @param {string} action @@ -251,9 +191,13 @@ function check_required_key(value, flag_name) { * @param {object} params */ function validate_create_user(params) { - check_required_username(params); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - validate_iam_path(params.iam_path, iam_constants.IAM_PARAMETER_NAME.IAM_PATH); + try { + check_required_username(params); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + validate_iam_path(params.iam_path, iam_constants.IAM_PARAMETER_NAME.IAM_PATH); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -261,7 +205,11 @@ function validate_create_user(params) { * @param {object} params */ function validate_get_user(params) { - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -269,10 +217,14 @@ function validate_get_user(params) { * @param {object} params */ function validate_update_user(params) { - check_required_username(params); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - validate_username(params.new_username, iam_constants.IAM_PARAMETER_NAME.NEW_USERNAME); - validate_iam_path(params.new_iam_path, iam_constants.IAM_PARAMETER_NAME.NEW_IAM_PATH); + try { + check_required_username(params); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + validation_utils.validate_username(params.new_username, iam_constants.IAM_PARAMETER_NAME.NEW_USERNAME); + validate_iam_path(params.new_iam_path, iam_constants.IAM_PARAMETER_NAME.NEW_IAM_PATH); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -280,8 +232,13 @@ function validate_update_user(params) { * @param {object} params */ function validate_delete_user(params) { - check_required_username(params); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + check_required_username(params); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + check_required_username(params); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } } /** @@ -289,9 +246,13 @@ function validate_delete_user(params) { * @param {object} params */ function validate_list_users(params) { - validate_marker(params.marker); - validate_max_items(params.max_items); - validate_iam_path(params.iam_path_prefix, iam_constants.IAM_PARAMETER_NAME.IAM_PATH_PREFIX); + try { + validate_marker(params.marker); + validate_max_items(params.max_items); + validate_iam_path(params.iam_path_prefix, iam_constants.IAM_PARAMETER_NAME.IAM_PATH_PREFIX); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -299,7 +260,11 @@ function validate_list_users(params) { * @param {object} params */ function validate_create_access_key(params) { - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -307,8 +272,12 @@ function validate_create_access_key(params) { * @param {object} params */ function validate_get_access_key_last_used(params) { - check_required_access_key_id(params); - validate_access_key_id(params.access_key); + try { + check_required_access_key_id(params); + validate_access_key_id(params.access_key); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -316,11 +285,15 @@ function validate_get_access_key_last_used(params) { * @param {object} params */ function validate_update_access_key(params) { - check_required_access_key_id(params); - check_required_status(params); - validate_access_key_id(params.access_key); - validate_status(params.status); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + check_required_access_key_id(params); + check_required_status(params); + validate_access_key_id(params.access_key); + validate_status(params.status); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -328,9 +301,13 @@ function validate_update_access_key(params) { * @param {object} params */ function validate_delete_access_key(params) { - check_required_access_key_id(params); - validate_access_key_id(params.access_key); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + check_required_access_key_id(params); + validate_access_key_id(params.access_key); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -338,9 +315,13 @@ function validate_delete_access_key(params) { * @param {object} params */ function validate_list_access_keys(params) { - validate_marker(params.marker); - validate_max_items(params.max_items); - validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + try { + validate_marker(params.marker); + validate_max_items(params.max_items); + validation_utils.validate_username(params.username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + } catch (err) { + translate_rpc_error(err); + } } /** @@ -352,64 +333,26 @@ function validate_list_access_keys(params) { * @param {string} parameter_name */ function validate_iam_path(input_path, parameter_name = iam_constants.IAM_PARAMETER_NAME.IAM_PATH) { - if (input_path === undefined) return; - // type check - _type_check_input('string', input_path, parameter_name); - // length check - const min_length = 1; - const max_length = 512; - _length_check_input(min_length, max_length, input_path, parameter_name); - // regex check - const valid_aws_path = input_path.startsWith('/') && input_path.endsWith('/') && AWS_IAM_PATH_REGEXP.test(input_path); - if (!valid_aws_path) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + - `It must begin and end with / and contain only alphanumeric characters and/or / characters.`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } - return true; -} - -/** - * validate_username will validate: - * 1. type - * 2. length - * 3. regex (from AWS docs) - * 4. additional internal restrictions - * @param {string} input_username - * @param {string} parameter_name - */ -function validate_username(input_username, parameter_name = iam_constants.IAM_PARAMETER_NAME.USERNAME) { - if (input_username === undefined) return; - // type check - _type_check_input('string', input_username, parameter_name); - // length check - const min_length = 1; - const max_length = 64; - _length_check_input(min_length, max_length, input_username, parameter_name); - // regex check - const valid_username = AWS_USERNAME_REGEXP.test(input_username); - if (!valid_username) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + - `It must contain only alphanumeric characters and/or the following: +=,.@_-`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } - // internal limitations - const invalid_internal_names = new Set(['anonymous', '/', '.']); - if (invalid_internal_names.has(input_username)) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + - `Should not be one of: ${[...invalid_internal_names].join(' ').toString()}`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); - } - if (input_username !== input_username.trim()) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + - `Must not contain leading or trailing spaces`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); + try { + if (input_path === undefined) return; + // type check + validation_utils._type_check_input('string', input_path, parameter_name); + // length check + const min_length = 1; + const max_length = 512; + validation_utils._length_check_input(min_length, max_length, input_path, parameter_name); + // regex check + const valid_aws_path = input_path.startsWith('/') && input_path.endsWith('/') && AWS_IAM_PATH_REGEXP.test(input_path); + if (!valid_aws_path) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + + `It must begin and end with / and contain only alphanumeric characters and/or / characters.`; + const { code, http_code, type } = IamError.ValidationError; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + return true; + } catch (err) { + translate_rpc_error(err); } - return true; } /** @@ -420,21 +363,25 @@ function validate_username(input_username, parameter_name = iam_constants.IAM_PA * @param {string} input_marker */ function validate_marker(input_marker) { - const parameter_name = 'Marker'; - if (input_marker === undefined) return; - // type check - _type_check_input('string', input_marker, parameter_name); - // length check - const min_length = 1; - _length_min_check_input(min_length, input_marker, parameter_name); - // regex check - const valid_marker = AWS_IAM_LIST_MARKER.test(input_marker); - if (!valid_marker) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. `; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); + try { + const parameter_name = 'Marker'; + if (input_marker === undefined) return; + // type check + validation_utils._type_check_input('string', input_marker, parameter_name); + // length check + const min_length = 1; + validation_utils._length_min_check_input(min_length, input_marker, parameter_name); + // regex check + const valid_marker = AWS_IAM_LIST_MARKER.test(input_marker); + if (!valid_marker) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. `; + const { code, http_code, type } = IamError.ValidationError; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + return true; + } catch (err) { + translate_rpc_error(err); } - return true; } /** @@ -448,7 +395,7 @@ function validate_max_items(input_max_items) { const parameter_name = 'MaxItems'; if (input_max_items === undefined) return; // type check - _type_check_input('number', input_max_items, parameter_name); + validation_utils._type_check_input('number', input_max_items, parameter_name); // value check const min_value = 1; const max_value = 1000; @@ -477,23 +424,27 @@ function validate_max_items(input_max_items) { * @param {string} input_access_key_id */ function validate_access_key_id(input_access_key_id) { - const parameter_name = 'AccessKeyId'; - if (input_access_key_id === undefined) return; - // type check - _type_check_input('string', input_access_key_id, parameter_name); - // length check - const min_length = 16; - const max_length = 128; - _length_check_input(min_length, max_length, input_access_key_id, parameter_name); - // regex check - const valid_access_key_id = AWS_IAM_ACCESS_KEY_INPUT_REGEXP.test(input_access_key_id); - if (!valid_access_key_id) { - const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + - `It must contain only alphanumeric characters`; - const { code, http_code, type } = IamError.ValidationError; - throw new IamError({ code, message: message_with_details, http_code, type }); + try { + const parameter_name = 'AccessKeyId'; + if (input_access_key_id === undefined) return; + // type check + validation_utils._type_check_input('string', input_access_key_id, parameter_name); + // length check + const min_length = 16; + const max_length = 128; + validation_utils._length_check_input(min_length, max_length, input_access_key_id, parameter_name); + // regex check + const valid_access_key_id = AWS_IAM_ACCESS_KEY_INPUT_REGEXP.test(input_access_key_id); + if (!valid_access_key_id) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + + `It must contain only alphanumeric characters`; + const { code, http_code, type } = IamError.ValidationError; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + return true; + } catch (err) { + translate_rpc_error(err); } - return true; } /** @@ -515,6 +466,17 @@ function validate_status(input_status) { return true; } +/** + * translate_rpc_error is used to translate the RPC error in-place + * @param {{ rpc_code: string; message: string; }} err + */ +function translate_rpc_error(err) { + if (err.rpc_code === 'VALIDATION_ERROR') { + const { code, http_code, type } = IamError.ValidationError; + throw new IamError({ code, message: err.message, http_code, type }); + } + throw err; +} // EXPORTS exports.format_iam_xml_date = format_iam_xml_date; @@ -525,7 +487,6 @@ exports.check_iam_path_was_set = check_iam_path_was_set; exports.parse_max_items = parse_max_items; exports.validate_params = validate_params; exports.validate_iam_path = validate_iam_path; -exports.validate_username = validate_username; exports.validate_marker = validate_marker; exports.validate_access_key_id = validate_access_key_id; exports.validate_status = validate_status; diff --git a/src/manage_nsfs/manage_nsfs_validations.js b/src/manage_nsfs/manage_nsfs_validations.js index c3f2bdf970..2ae0d76a36 100644 --- a/src/manage_nsfs/manage_nsfs_validations.js +++ b/src/manage_nsfs/manage_nsfs_validations.js @@ -13,8 +13,8 @@ const { throw_cli_error, get_options_from_file, get_boolean_or_string_value, get is_name_update, is_access_key_update } = 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, DIAGNOSE_ACTIONS, UPGRADE_ACTIONS } = require('../manage_nsfs/manage_nsfs_constants'); -const iam_utils = require('../endpoint/iam/iam_utils'); const { check_root_account_owns_user } = require('../nc/nc_utils'); +const { validate_username } = require('../util/validation_utils'); ///////////////////////////// //// GENERAL VALIDATIONS //// @@ -317,15 +317,14 @@ function validate_account_name(type, action, input_options_with_data) { try { if (action === ACTIONS.ADD) { account_name = String(input_options_with_data.name); - iam_utils.validate_username(account_name, 'name'); + validate_username(account_name, 'name'); } else if (action === ACTIONS.UPDATE && input_options_with_data.new_name !== undefined) { account_name = String(input_options_with_data.new_name); - iam_utils.validate_username(account_name, 'new_name'); + validate_username(account_name, 'new_name'); } } catch (err) { if (err instanceof ManageCLIError) throw err; - // we receive IAMError and replace it to ManageCLIError - // we do not use the mapping errors because it is a general error ValidationError + // we replace it to ManageCLIError const detail = err.message; throw_cli_error(ManageCLIError.InvalidAccountName, detail); } diff --git a/src/sdk/bucketspace_fs.js b/src/sdk/bucketspace_fs.js index 9d9047a746..76df15167b 100644 --- a/src/sdk/bucketspace_fs.js +++ b/src/sdk/bucketspace_fs.js @@ -294,11 +294,6 @@ class BucketSpaceFS extends BucketSpaceSimpleFS { if (!sdk.requesting_account.allow_bucket_creation) { throw new RpcError('UNAUTHORIZED', 'Not allowed to create new buckets'); } - // currently we do not allow IAM account to create a bucket (temporary) - if (sdk.requesting_account.owner !== undefined) { - dbg.warn('create_bucket: account is IAM account (currently not allowed to create buckets)'); - throw new RpcError('UNAUTHORIZED', 'Not allowed to create new buckets'); - } if (!sdk.requesting_account.nsfs_account_config || !sdk.requesting_account.nsfs_account_config.new_buckets_path) { throw new RpcError('MISSING_NSFS_ACCOUNT_CONFIGURATION'); } @@ -345,7 +340,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS { _id: mongo_utils.mongoObjectId(), name, tag: js_utils.default_value(tag, undefined), - owner_account: account._id, + owner_account: account.owner ? account.owner : account._id, // The account is the owner of the buckets that were created by it or by its users. creator: account._id, versioning: config.NSFS_VERSIONING_ENABLED && lock_enabled ? 'ENABLED' : 'DISABLED', object_lock_configuration: config.WORM_ENABLED ? { diff --git a/src/test/system_tests/test_utils.js b/src/test/system_tests/test_utils.js index 9d16e648fa..7a6b097b5c 100644 --- a/src/test/system_tests/test_utils.js +++ b/src/test/system_tests/test_utils.js @@ -5,9 +5,11 @@ const fs = require('fs'); const _ = require('lodash'); const path = require('path'); const http = require('http'); +const https = require('https'); const P = require('../../util/promise'); const config = require('../../../config'); const { S3 } = require('@aws-sdk/client-s3'); +const { IAMClient } = require('@aws-sdk/client-iam'); const os_utils = require('../../util/os_utils'); const fs_utils = require('../../util/fs_utils'); const nb_native = require('../../util/nb_native'); @@ -416,6 +418,20 @@ function generate_s3_client(access_key, secret_key, endpoint) { endpoint }); } + +function generate_iam_client(access_key, secret_key, endpoint) { + const httpsAgent = new https.Agent({ rejectUnauthorized: false }); // disable SSL certificate validation + return new IAMClient({ + region: config.DEFAULT_REGION, + credentials: { + accessKeyId: access_key, + secretAccessKey: secret_key, + }, + endpoint, + requestHandler: new NodeHttpHandler({ httpsAgent }), + }); +} + /** * generate_nsfs_account generate an nsfs account and returns its credentials * if the admin flag is received (in the options object) the function will not create @@ -720,6 +736,7 @@ exports.empty_and_delete_buckets = empty_and_delete_buckets; exports.disable_accounts_s3_access = disable_accounts_s3_access; exports.generate_s3_policy = generate_s3_policy; exports.generate_s3_client = generate_s3_client; +exports.generate_iam_client = generate_iam_client; exports.invalid_nsfs_root_permissions = invalid_nsfs_root_permissions; exports.get_coretest_path = get_coretest_path; exports.exec_manage_cli = exec_manage_cli; diff --git a/src/test/unit_tests/jest_tests/test_iam_utils.test.js b/src/test/unit_tests/jest_tests/test_iam_utils.test.js index cb20ef27de..56dc1d67cb 100644 --- a/src/test/unit_tests/jest_tests/test_iam_utils.test.js +++ b/src/test/unit_tests/jest_tests/test_iam_utils.test.js @@ -312,132 +312,6 @@ describe('validate_user_input_iam', () => { }); }); - describe('validate_username', () => { - const min_length = 1; - const max_length = 64; - it('should return true when username is undefined', () => { - let dummy_username; - const res = iam_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - expect(res).toBeUndefined(); - }); - - it('should return true when username is at the min or max length', () => { - expect(iam_utils.validate_username('a', iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); - expect(iam_utils.validate_username('a'.repeat(max_length), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); - }); - - it('should return true when username is within the length constraint', () => { - expect(iam_utils.validate_username('a'.repeat(min_length + 1), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); - expect(iam_utils.validate_username('a'.repeat(max_length - 1), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); - }); - - it('should return true when username is valid', () => { - const dummy_username = 'Robert'; - const res = iam_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - expect(res).toBe(true); - }); - - it('should throw error when username is invalid - contains invalid character', () => { - try { - iam_utils.validate_username('{}', iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error when username is invalid - internal limitation (anonymous)', () => { - try { - iam_utils.validate_username('anonymous', iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error when username is invalid - internal limitation (with leading or trailing spaces)', () => { - try { - iam_utils.validate_username(' name-with-spaces ', iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error when username is too short', () => { - try { - const dummy_username = ''; - iam_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error when username is too long', () => { - try { - const dummy_username = 'A'.repeat(max_length + 1); - iam_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error for invalid input types (null)', () => { - try { - // @ts-ignore - const invalid_username = null; - iam_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error for invalid input types (number)', () => { - try { - const invalid_username = 1; - // @ts-ignore - iam_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error for invalid input types (object)', () => { - try { - const invalid_username = {}; - // @ts-ignore - iam_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - - it('should throw error for invalid input types (boolean)', () => { - try { - const invalid_username = false; - // @ts-ignore - iam_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); - throw new NoErrorThrownError(); - } catch (err) { - expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.ValidationError.code); - } - }); - }); - describe('validate_marker', () => { const min_length = 1; it('should return true when marker is undefined', () => { diff --git a/src/test/unit_tests/jest_tests/test_validation_utils.test.js b/src/test/unit_tests/jest_tests/test_validation_utils.test.js new file mode 100644 index 0000000000..5623ccddf6 --- /dev/null +++ b/src/test/unit_tests/jest_tests/test_validation_utils.test.js @@ -0,0 +1,138 @@ +/* Copyright (C) 2024 NooBaa */ +/* eslint-disable max-lines-per-function */ +'use strict'; +const validation_utils = require('../../../util/validation_utils'); +const iam_constants = require('../../../endpoint/iam/iam_constants'); +const RpcError = require('../../../rpc/rpc_error'); + +class NoErrorThrownError extends Error {} + +describe('validate_user_input', () => { + const RPC_CODE_VALIDATION_ERROR = 'VALIDATION_ERROR'; + describe('validate_username', () => { + const min_length = 1; + const max_length = 64; + it('should return true when username is undefined', () => { + let dummy_username; + const res = validation_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + expect(res).toBeUndefined(); + }); + + it('should return true when username is at the min or max length', () => { + expect(validation_utils.validate_username('a', iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); + expect(validation_utils.validate_username('a'.repeat(max_length), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); + }); + + it('should return true when username is within the length constraint', () => { + expect(validation_utils.validate_username('a'.repeat(min_length + 1), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); + expect(validation_utils.validate_username('a'.repeat(max_length - 1), iam_constants.IAM_PARAMETER_NAME.USERNAME)).toBe(true); + }); + + it('should return true when username is valid', () => { + const dummy_username = 'Robert'; + const res = validation_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + expect(res).toBe(true); + }); + + it('should throw error when username is invalid - contains invalid character', () => { + try { + validation_utils.validate_username('{}', iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + + it('should throw error when username is invalid - internal limitation (anonymous)', () => { + try { + validation_utils.validate_username('anonymous', iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error when username is invalid - internal limitation (with leading or trailing spaces)', () => { + try { + validation_utils.validate_username(' name-with-spaces ', iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error when username is too short', () => { + try { + const dummy_username = ''; + validation_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error when username is too long', () => { + try { + const dummy_username = 'A'.repeat(max_length + 1); + validation_utils.validate_username(dummy_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error for invalid input types (null)', () => { + try { + // @ts-ignore + const invalid_username = null; + validation_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error for invalid input types (number)', () => { + try { + const invalid_username = 1; + // @ts-ignore + validation_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error for invalid input types (object)', () => { + try { + const invalid_username = {}; + // @ts-ignore + validation_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + + it('should throw error for invalid input types (boolean)', () => { + try { + const invalid_username = false; + // @ts-ignore + validation_utils.validate_username(invalid_username, iam_constants.IAM_PARAMETER_NAME.USERNAME); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(RpcError); + expect(err.rpc_code).toBe(RPC_CODE_VALIDATION_ERROR); + } + }); + }); +}); diff --git a/src/test/unit_tests/nc_coretest.js b/src/test/unit_tests/nc_coretest.js index 61e0202d2b..71e65059c2 100644 --- a/src/test/unit_tests/nc_coretest.js +++ b/src/test/unit_tests/nc_coretest.js @@ -48,8 +48,10 @@ const EMAIL = NC_CORETEST; const PASSWORD = NC_CORETEST; const http_port = 6001; const https_port = 6443; +const https_port_iam = 7005; const http_address = `http://localhost:${http_port}`; const https_address = `https://localhost:${https_port}`; +const https_address_iam = `https://localhost:${https_port_iam}`; const FIRST_BUCKET = 'first.bucket'; const NC_CORETEST_STORAGE_PATH = p.join(TMP_PATH, 'nc_coretest_storage_root_path/'); @@ -68,8 +70,8 @@ let current_setup_options = {}; * @param {object} setup_options */ function setup(setup_options = {}) { - console.log(`in setup - variables values: _setup ${_setup} _nsfs_process ${_nsfs_process}`); - current_setup_options = setup_options; + console.log(`in setup - variables values: _setup ${_setup} _nsfs_process ${_nsfs_process} ` + + `setup_options`, setup_options); if (_setup) return; _setup = true; @@ -84,6 +86,7 @@ function setup(setup_options = {}) { await start_nsfs_process(setup_options); // TODO - run health + current_setup_options = setup_options; await announce(`nc coretest ready... (took ${((Date.now() - start) / 1000).toFixed(1)} sec)`); }); @@ -109,8 +112,8 @@ function setup(setup_options = {}) { */ async function start_nsfs_process(setup_options) { console.log(`in start_nsfs_process - variables values: _setup ${_setup} _nsfs_process ${_nsfs_process}`); - const { forks, debug } = setup_options; - console.log(`setup_options: forks ${forks} debug ${debug}`); + const { forks, debug, should_run_iam } = setup_options; + console.log(`setup_options: forks ${forks} debug ${debug} should_run_iam ${should_run_iam}`); if (_nsfs_process) return; await announce('start nsfs script'); const logStream = fs.createWriteStream('nsfs_integration_test_log.txt', { flags: 'a' }); @@ -124,6 +127,10 @@ async function start_nsfs_process(setup_options) { arguments_for_command.push('--debug'); arguments_for_command.push(`${debug}`); } + if (should_run_iam && https_port_iam) { + arguments_for_command.push('--https_port_iam'); + arguments_for_command.push(`${https_port_iam}`); + } nsfs_process = child_process.spawn('node', arguments_for_command, { detached: true }); @@ -267,6 +274,14 @@ function get_https_address() { return https_address; } +/** + * get_iam_https_address return nc coretest https_address_iam variable + * @returns {string} + */ +function get_iam_https_address() { + return https_address_iam; +} + /////////////////////////////////// ///////// HELPER FUNCTIONS //////// /////////////////////////////////// @@ -532,6 +547,7 @@ exports.get_dbg_level = get_dbg_level; exports.rpc_client = rpc_cli_funcs_to_manage_nsfs_cli_cmds; exports.get_http_address = get_http_address; exports.get_https_address = get_https_address; +exports.get_iam_https_address = get_iam_https_address; exports.get_admin_mock_account_details = get_admin_mock_account_details; exports.NC_CORETEST_CONFIG_DIR_PATH = NC_CORETEST_CONFIG_DIR_PATH; exports.NC_CORETEST_CONFIG_FS = NC_CORETEST_CONFIG_FS; diff --git a/src/test/unit_tests/nc_index.js b/src/test/unit_tests/nc_index.js index 1bee9f6323..de1f942dda 100644 --- a/src/test/unit_tests/nc_index.js +++ b/src/test/unit_tests/nc_index.js @@ -21,6 +21,8 @@ require('./test_bucketspace_versioning'); require('./test_nc_bucket_logging'); require('./test_nc_online_upgrade_s3_integrations'); +// running with iam port +require('./test_nc_iam_basic_integration.js'); // please notice that we use a different setup // running with a couple of forks - please notice and add only relevant tests here require('./test_nc_with_a_couple_of_forks.js'); // please notice that we use a different setup diff --git a/src/test/unit_tests/test_bucketspace_fs.js b/src/test/unit_tests/test_bucketspace_fs.js index 037ae3a0e8..203bd2d4cb 100644 --- a/src/test/unit_tests/test_bucketspace_fs.js +++ b/src/test/unit_tests/test_bucketspace_fs.js @@ -431,16 +431,17 @@ mocha.describe('bucketspace_fs', function() { assert.ok(err.rpc_code === 'UNAUTHORIZED'); } }); - mocha.it('should fail - create bucket by iam account', async function() { - // currently we do not allow IAM accounts to create buckets - try { - const param = { name: test_bucket_iam_account }; - const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1); - await bucketspace_fs.create_bucket(param, dummy_object_sdk_for_iam_account); - assert.fail('should have failed with UNAUTHORIZED bucket creation'); - } catch (err) { - assert.ok(err.rpc_code === 'UNAUTHORIZED'); - } + mocha.it('create bucket by iam account', async function() { + const param = { name: test_bucket_iam_account }; + const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1); + await bucketspace_fs.create_bucket(param, dummy_object_sdk_for_iam_account); + const bucket_config_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, param.name); + const stat1 = await fs.promises.stat(bucket_config_path); + assert.equal(stat1.nlink, 1); + + // check that the owner is the account (not the user) + const bucket = await bucketspace_fs.read_bucket_sdk_info(param); + assert.ok(bucket.owner_account.id === account_iam_user1.owner); }); mocha.after(async function() { await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket}`); diff --git a/src/test/unit_tests/test_nc_iam_basic_integration.js b/src/test/unit_tests/test_nc_iam_basic_integration.js new file mode 100644 index 0000000000..182a97227c --- /dev/null +++ b/src/test/unit_tests/test_nc_iam_basic_integration.js @@ -0,0 +1,240 @@ +/* Copyright (C) 2024 NooBaa */ +/* eslint-disable max-statements */ +'use strict'; + +const path = require('path'); +const _ = require('lodash'); +const mocha = require('mocha'); +const assert = require('assert'); +const fs_utils = require('../../util/fs_utils'); +const { TMP_PATH, generate_nsfs_account, get_new_buckets_path_by_test_env, generate_iam_client, + get_coretest_path } = require('../system_tests/test_utils'); +const { ListUsersCommand, CreateUserCommand, GetUserCommand, UpdateUserCommand, DeleteUserCommand, + ListAccessKeysCommand, CreateAccessKeyCommand, GetAccessKeyLastUsedCommand, + UpdateAccessKeyCommand, DeleteAccessKeyCommand } = require('@aws-sdk/client-iam'); +const { ACCESS_KEY_STATUS_ENUM } = require('../../endpoint/iam/iam_constants'); + +const coretest_path = get_coretest_path(); +const coretest = require(coretest_path); +const setup_options = { should_run_iam: true, https_port_iam: 7005, debug: 5 }; +coretest.setup(setup_options); +const { rpc_client, EMAIL, get_current_setup_options, stop_nsfs_process, start_nsfs_process } = coretest; + +const CORETEST_ENDPOINT_IAM = coretest.get_iam_https_address(); + +const config_root = path.join(TMP_PATH, 'test_nc_iam'); +// on NC - new_buckets_path is full absolute path +// on Containerized - new_buckets_path is the directory +const new_bucket_path_param = get_new_buckets_path_by_test_env(config_root, '/'); + +let iam_account; + +mocha.describe('IAM basic integration tests - happy path', async function() { + this.timeout(50000); // eslint-disable-line no-invalid-this + + mocha.before(async () => { + // we want to make sure that we run this test with a couple of forks (by default setup it is 0) + const current_setup_options = get_current_setup_options(); + const same_setup = _.isEqual(current_setup_options, setup_options); + if (!same_setup) { + console.log('current_setup_options', current_setup_options, 'same_setup', same_setup); + await stop_nsfs_process(); + await start_nsfs_process(setup_options); + } + + // needed details for creating the account (and then the client) + await fs_utils.create_fresh_path(new_bucket_path_param); + await fs_utils.file_must_exist(new_bucket_path_param); + const res = await generate_nsfs_account(rpc_client, EMAIL, new_bucket_path_param, { admin: true }); + iam_account = generate_iam_client(res.access_key, res.secret_key, CORETEST_ENDPOINT_IAM); + }); + + mocha.after(async () => { + fs_utils.folder_delete(`${config_root}`); + }); + + mocha.describe('IAM User API', async function() { + const username = 'Asahi'; + const new_username = 'Botan'; + + mocha.it('list users - should be empty', async function() { + const input = {}; + const command = new ListUsersCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.Users.length, 0); + }); + + mocha.it('create a user', async function() { + const input = { + UserName: username + }; + const command = new CreateUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.User.UserName, username); + + // verify it using list users + const input2 = {}; + const command2 = new ListUsersCommand(input2); + const response2 = await iam_account.send(command2); + assert.equal(response2.$metadata.httpStatusCode, 200); + assert.equal(response2.Users.length, 1); + assert.equal(response2.Users[0].UserName, username); + }); + + mocha.it('get a user', async function() { + const input = { + UserName: username + }; + const command = new GetUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.User.UserName, username); + }); + + mocha.it('update a user', async function() { + const input = { + NewUserName: new_username, + UserName: username + }; + const command = new UpdateUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + + // verify it using list users + const input2 = {}; + const command2 = new ListUsersCommand(input2); + const response2 = await iam_account.send(command2); + _check_status_code_ok(response2); + assert.equal(response2.Users.length, 1); + assert.equal(response2.Users[0].UserName, new_username); + }); + + mocha.it('delete a user', async function() { + const input = { + UserName: new_username // delete a user after its username was updated + }; + const command = new DeleteUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + }); + }); + + mocha.describe('IAM Access Key API', async function() { + const username2 = 'Fuji'; + let access_key_id; + + mocha.before(async () => { + // create a user + const input = { + UserName: username2 + }; + const command = new CreateUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + }); + + mocha.after(async () => { + // delete a user + const input = { + UserName: username2 + }; + const command = new DeleteUserCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + // note: if somehow the delete access key would fail, then deleting the user would also fail + // (as we can delete a user only after its access keys were deleted) + }); + + mocha.it('list access keys - should be empty', async function() { + const input = { + UserName: username2 + }; + const command = new ListAccessKeysCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.AccessKeyMetadata.length, 0); + }); + + mocha.it('create access keys', async function() { + const input = { + UserName: username2 + }; + const command = new CreateAccessKeyCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.AccessKey.UserName, username2); + assert(response.AccessKey.AccessKeyId !== undefined); + access_key_id = response.AccessKey.AccessKeyId; + assert(response.AccessKey.SecretAccessKey !== undefined); + assert.equal(response.AccessKey.Status, ACCESS_KEY_STATUS_ENUM.ACTIVE); + + // verify it using list access keys + const input2 = { + UserName: username2 + }; + const command2 = new ListAccessKeysCommand(input2); + const response2 = await iam_account.send(command2); + _check_status_code_ok(response2); + assert.equal(response2.AccessKeyMetadata.length, 1); + assert.equal(response2.AccessKeyMetadata[0].UserName, username2); + assert.equal(response2.AccessKeyMetadata[0].AccessKeyId, access_key_id); + assert.equal(response2.AccessKeyMetadata[0].Status, ACCESS_KEY_STATUS_ENUM.ACTIVE); + }); + + mocha.it('get access key (last used)', async function() { + const input = { + AccessKeyId: access_key_id + }; + const command = new GetAccessKeyLastUsedCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + assert.equal(response.UserName, username2); + assert(response.AccessKeyLastUsed.LastUsedDate !== undefined); + assert(response.AccessKeyLastUsed.ServiceName !== undefined); + assert(response.AccessKeyLastUsed.Region !== undefined); + }); + + mocha.it('update access keys (active to inactive)', async function() { + const input = { + UserName: username2, + AccessKeyId: access_key_id, + Status: ACCESS_KEY_STATUS_ENUM.INACTIVE + }; + const command = new UpdateAccessKeyCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + + // verify it using list access keys + const input2 = { + UserName: username2 + }; + const command2 = new ListAccessKeysCommand(input2); + const response2 = await iam_account.send(command2); + _check_status_code_ok(response2); + assert.equal(response2.AccessKeyMetadata.length, 1); + assert.equal(response2.AccessKeyMetadata[0].UserName, username2); + assert.equal(response2.AccessKeyMetadata[0].AccessKeyId, access_key_id); + assert.equal(response2.AccessKeyMetadata[0].Status, ACCESS_KEY_STATUS_ENUM.INACTIVE); + }); + + mocha.it('delete access keys', async function() { + const input = { + UserName: username2, + AccessKeyId: access_key_id + }; + const command = new DeleteAccessKeyCommand(input); + const response = await iam_account.send(command); + _check_status_code_ok(response); + }); + }); +}); + +/** + * _check_status_code_ok is an helper function to check that we got an response from the server + * @param {{ $metadata: { httpStatusCode: number; }; }} response + */ +function _check_status_code_ok(response) { + assert.equal(response.$metadata.httpStatusCode, 200); +} diff --git a/src/util/validation_utils.js b/src/util/validation_utils.js new file mode 100644 index 0000000000..ad50353073 --- /dev/null +++ b/src/util/validation_utils.js @@ -0,0 +1,110 @@ +/* Copyright (C) 2024 NooBaa */ +'use strict'; + +const _ = require('lodash'); +const { AWS_USERNAME_REGEXP } = require('../util/string_utils'); +const RpcError = require('../rpc/rpc_error'); +const iam_constants = require('../endpoint/iam/iam_constants'); + +/** + * _type_check_input checks that the input is the same as needed + * @param {string} input_type + * @param {string | number} input_value + * @param {string} parameter_name + */ +function _type_check_input(input_type, input_value, parameter_name) { + if (typeof input_value !== input_type) { + const message_with_details = `1 validation error detected: Value ${input_value} at ` + + `'${parameter_name}' failed to satisfy constraint: Member must be ${input_type}`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } +} + +/** + * _length_check_input checks that the input length is between the min and the max value + * @param {number} min_length + * @param {number} max_length + * @param {string} input_value + * @param {string} parameter_name + */ +function _length_check_input(min_length, max_length, input_value, parameter_name) { + _length_min_check_input(min_length, input_value, parameter_name); + _length_max_check_input(max_length, input_value, parameter_name); +} + +/** + * _length_min_check_input checks if the input is lower than the min length + * @param {number} min_length + * @param {any} input_value + * @param {string} parameter_name + */ +function _length_min_check_input(min_length, input_value, parameter_name) { + const input_length = input_value.length; + if (input_length < min_length) { + const message_with_details = `Invalid length for parameter ${parameter_name}, ` + + `value: ${input_length}, valid min length: ${min_length}`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } +} + +/** + * _length_max_check_input checks if the input is higher than the max length + * @param {number} max_length + * @param {any} input_value + * @param {string} parameter_name + */ +function _length_max_check_input(max_length, input_value, parameter_name) { + const input_length = input_value.length; + if (input_length > max_length) { + const message_with_details = `1 validation error detected: Value ${input_value} at ` + + `'${parameter_name}' failed to satisfy constraint:` + + `Member must have length less than or equal to ${max_length}`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } +} + +/** + * validate_username will validate: + * 1. type + * 2. length + * 3. regex (from AWS docs) + * 4. additional internal restrictions + * @param {string} input_username + * @param {string} parameter_name + */ +function validate_username(input_username, parameter_name = iam_constants.IAM_PARAMETER_NAME.USERNAME) { + if (input_username === undefined) return; + // type check + _type_check_input('string', input_username, parameter_name); + // length check + const min_length = 1; + const max_length = 64; + _length_check_input(min_length, max_length, input_username, parameter_name); + // regex check + const valid_username = AWS_USERNAME_REGEXP.test(input_username); + if (!valid_username) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + + `It must contain only alphanumeric characters and/or the following: +=,.@_-`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } + // internal limitations + const invalid_internal_names = new Set(['anonymous', '/', '.']); + if (invalid_internal_names.has(input_username)) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + + `Should not be one of: ${[...invalid_internal_names].join(' ').toString()}`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } + if (input_username !== input_username.trim()) { + const message_with_details = `The specified value for ${_.lowerFirst(parameter_name)} is invalid. ` + + `Must not contain leading or trailing spaces`; + throw new RpcError('VALIDATION_ERROR', message_with_details); + } + return true; +} + +// EXPORTS +exports.validate_username = validate_username; +exports._type_check_input = _type_check_input; +exports._length_check_input = _length_check_input; +exports._length_min_check_input = _length_min_check_input; +