diff --git a/docs/design/iam.md b/docs/design/iam.md index f31b25718c..2b2a8c32bd 100644 --- a/docs/design/iam.md +++ b/docs/design/iam.md @@ -61,7 +61,7 @@ At this point we will not support additional IAM resources (group, policy, role, - `owner` vs `creator` - owner is permission wise, creator is for internal information. -### The user creation flow: +### The user and access keys creation flow: One root and one user (just to understand the basic API relations and hierarchy) ![One root and one user diagram](https://github.com/noobaa/noobaa-core/assets/57721533/b77ade91-11dd-415c-b3f0-5f3f1747a694) diff --git a/docs/dev_guide/nc_nsfs_iam_developer_doc.md b/docs/dev_guide/nc_nsfs_iam_developer_doc.md index 64a5c215d9..a684f60383 100644 --- a/docs/dev_guide/nc_nsfs_iam_developer_doc.md +++ b/docs/dev_guide/nc_nsfs_iam_developer_doc.md @@ -14,13 +14,41 @@ This will be the argument for: - `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 `. -2. Start the NSFS server (using debug mode and the port for IAM): `sudo node src/cmd/nsfs --debug 5 --https_port_iam 7005` +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: -`alias s3-nc-user-1-iam='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:7005'`. -11. Use AWS CLI to send requests to the IAM service, for example: - `s3-nc-user-1-iam iam create-user --user-name Bob --path /division_abc/subdivision_xyz/` - `s3-nc-user-1-iam iam get-user --user-name Bob` - `s3-nc-user-1-iam iam update-user --user-name Bob --new-path /division_abc/subdivision_abc/` - `s3-nc-user-1-iam iam delete-user --user-name Bob` - `s3-nc-user-1-iam iam list-users` \ No newline at end of file +`alias nc-user-1-iam='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:7005'`. +5. Use AWS CLI to send requests to the IAM service, for example: + `nc-user-1-iam iam create-user --user-name Bob --path /division_abc/subdivision_xyz/` + `nc-user-1-iam iam get-user --user-name Bob` + `nc-user-1-iam iam update-user --user-name Bob --new-path /division_abc/subdivision_abc/` + `nc-user-1-iam iam delete-user --user-name Bob` + `nc-user-1-iam iam list-users` + + `nc-user-1-iam iam create-access-key --user-name Bob` + `nc-user-1-iam iam update-access-key --access-key-id --user-name Bob --status Inactive` + `nc-user-1-iam iam delete-access-key --access-key-id --user-name Bob` + `nc-user-1-iam iam list-access-keys --user-name Bob` + +Create the alias for IAM service for the user that was created (with its access keys): +`alias nc-user-1-iam-regular='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:7005'`. +`nc-user-1-iam-regular iam get-access-key-last-used --access-key-id ` + +### Demo Examples: +#### Deactivate Access Key: +`alias nc-user-1-iam-regular='AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= aws --no-verify-ssl --endpoint-url https://localhost:6443'` (port for s3) +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://>` +3. List bucket (use s3 service)`nc-user-1-s3-regular s3 ls` +4. List access keys (use IAM service) `nc-user-1-iam-regular iam list-access-keys` +5. Deactivate access keys: `nc-user-1-iam iam update-access-key --access-key-id --user-name --status Inactive` +6. It should throw an error for both s3 service (`nc-user-1-s3-regular s3 ls`) and iam service (`nc-user-1-iam-regular iam list-access-keys`) that uses the deactivated access key. +Note: Currently we clean the cache after update, but it happens for the specific endpoint, if there are more endpoints (using forks) developers can change the expiry cache in the line `expiry_ms: 1` inside `account_cache` (currently inside object_sdk). + +#### Rename Username: +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 diff --git a/src/endpoint/iam/iam_errors.js b/src/endpoint/iam/iam_errors.js index e9722bf187..04fb57eb6d 100644 --- a/src/endpoint/iam/iam_errors.js +++ b/src/endpoint/iam/iam_errors.js @@ -77,6 +77,12 @@ IamError.InvalidClientTokenId = Object.freeze({ http_code: 403, type: error_type_enum.SENDER, }); +IamError.InvalidClientTokenIdInactiveAccessKey = Object.freeze({ + code: 'InvalidClientTokenId', + message: 'The security token included in the request is invalid.', + http_code: 403, + type: error_type_enum.SENDER, +}); IamError.NotAuthorized = Object.freeze({ code: 'NotAuthorized', message: 'You do not have permission to perform this action.', @@ -122,13 +128,20 @@ IamError.NotImplemented = Object.freeze({ }); // These errors were copied from IAM APIs errors +// Users // CreateUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html#API_CreateUser_Errors -// DeleteUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html#API_DeleteUser_Errors // GetUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetUser.html#API_GetUser_Errors // UpdateUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateUser.html#API_UpdateUser_Errors +// DeleteUser errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html#API_DeleteUser_Errors // ListUsers errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUsers.html +// Access keys +// CreateAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateAccessKey.html +// GetAccessKeyLastUsed errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetAccessKeyLastUsed.html (nothing appears) +// UpdateAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html +// DeleteAccessKey errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteAccessKey.html +// ListAccessKeys errors https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListAccessKeys.html IamError.ConcurrentModification = Object.freeze({ - code: 'EntityAlreadyExists', + code: 'ConcurrentModification', message: 'The request was rejected because multiple requests to change this object were submitted simultaneously. Wait a few minutes and submit your request again.', http_code: 409, type: error_type_enum.SENDER, @@ -140,13 +153,13 @@ IamError.EntityAlreadyExists = Object.freeze({ type: error_type_enum.SENDER, }); IamError.InvalidInput = Object.freeze({ - code: 'EntityAlreadyExists', + code: 'InvalidInput', message: 'The request was rejected because an invalid or out-of-range value was supplied for an input parameter.', http_code: 400, type: error_type_enum.SENDER, }); IamError.LimitExceeded = Object.freeze({ - code: 'EntityAlreadyExists', + code: 'LimitExceeded', message: 'The request was rejected because it attempted to create resources beyond the current AWS account limits. The error message describes the limit exceeded.', http_code: 409, type: error_type_enum.SENDER, @@ -175,59 +188,14 @@ IamError.EntityTemporarilyUnmodifiable = Object.freeze({ http_code: 409, type: error_type_enum.SENDER, }); -// These errors were actually send after performing IAM actions -IamError.AccessDenied = Object.freeze({ - code: 'AccessDenied', - message: 'user is not authorized to perform action on resource', - http_code: 400, - type: error_type_enum.SENDER, -}); - // These errors were copied from STS errors -// TODO - can be deleted after verifying we will not use them -IamError.InvalidParameterCombination = Object.freeze({ - code: 'InvalidParameterCombination', - message: 'Parameters that must not be used together were used together.', - http_code: 400, - type: error_type_enum.SENDER, -}); IamError.InvalidParameterValue = Object.freeze({ code: 'InvalidParameterValue', message: 'An invalid or out-of-range value was supplied for the input parameter.', http_code: 400, type: error_type_enum.SENDER, }); -IamError.InvalidQueryParameter = Object.freeze({ - code: 'InvalidQueryParameter', - message: 'The AWS query string is malformed or does not adhere to AWS standards.', - http_code: 400, - type: error_type_enum.SENDER, -}); -IamError.MalformedQueryString = Object.freeze({ - code: 'MalformedQueryString', - message: 'The query string contains a syntax error.', - http_code: 404, - type: error_type_enum.SENDER, -}); -IamError.MissingAction = Object.freeze({ - code: 'MissingAction', - message: 'The request is missing an action or a required parameter.', - http_code: 400, - type: error_type_enum.SENDER, -}); -IamError.MissingAuthenticationToken = Object.freeze({ - code: 'MissingAuthenticationToken', - message: 'The request must contain either a valid (registered) AWS access key ID or X.509 certificate.', - http_code: 403, - type: error_type_enum.SENDER, -}); -IamError.MissingParameter = Object.freeze({ - code: 'MissingParameter', - message: 'A required parameter for the specified action is not supplied.', - http_code: 400, - type: error_type_enum.SENDER, -}); IamError.ExpiredToken = Object.freeze({ code: 'ExpiredToken', message: 'The security token included in the request is expired', diff --git a/src/endpoint/iam/iam_rest.js b/src/endpoint/iam/iam_rest.js index 4c207db35f..31d578784b 100644 --- a/src/endpoint/iam/iam_rest.js +++ b/src/endpoint/iam/iam_rest.js @@ -16,7 +16,8 @@ const IAM_XML_ROOT_ATTRS = Object.freeze({ const RPC_ERRORS_TO_IAM = Object.freeze({ SIGNATURE_DOES_NOT_MATCH: IamError.AccessDeniedException, UNAUTHORIZED: IamError.AccessDeniedException, - INVALID_ACCESS_KEY_ID: IamError.AccessDeniedException, + INVALID_ACCESS_KEY_ID: IamError.InvalidClientTokenId, + DEACTIVATED_ACCESS_KEY_ID: IamError.InvalidClientTokenIdInactiveAccessKey, NO_SUCH_ACCOUNT: IamError.AccessDeniedException, NO_SUCH_ROLE: IamError.AccessDeniedException }); @@ -44,7 +45,7 @@ const IAM_OPS = js_utils.deep_freeze({ post_list_users: require('./ops/iam_list_users'), // access key CRUD post_create_access_key: require('./ops/iam_create_access_key'), - post_get_access_key_last_used: require('./ops/iam_get_access_key'), + post_get_access_key_last_used: require('./ops/get_access_key_last_used'), post_update_access_key: require('./ops/iam_update_access_key'), post_delete_access_key: require('./ops/iam_delete_access_key'), post_list_access_keys: require('./ops/iam_list_access_keys'), diff --git a/src/endpoint/iam/iam_utils.js b/src/endpoint/iam/iam_utils.js index 40b652d23a..e2d4f5f54f 100644 --- a/src/endpoint/iam/iam_utils.js +++ b/src/endpoint/iam/iam_utils.js @@ -24,6 +24,18 @@ const ACTION_MESSAGE_TITLE_MAP = { 'list_access_keys': 'ListAccessKeys', }; +const MAX_NUMBER_OF_ACCESS_KEYS = 2; + +const access_key_status_enum = Object.freeze({ + ACTIVE: 'Active', + INACTIVE: 'Inactive', +}); + +const identity_enum = Object.freeze({ + ROOT_ACCOUNT: 'ROOT_ACCOUNT', + USER: 'USER', +}); + /** * format_iam_xml_date return the date without milliseconds * @param {any} input @@ -73,3 +85,6 @@ exports.IAM_DEFAULT_PATH = IAM_DEFAULT_PATH; exports.AWS_NOT_USED = AWS_NOT_USED; exports.get_action_message_title = get_action_message_title; exports.check_iam_path_was_set = check_iam_path_was_set; +exports.MAX_NUMBER_OF_ACCESS_KEYS = MAX_NUMBER_OF_ACCESS_KEYS; +exports.access_key_status_enum = access_key_status_enum; +exports.identity_enum = identity_enum; diff --git a/src/endpoint/iam/ops/iam_get_access_key.js b/src/endpoint/iam/ops/get_access_key_last_used.js similarity index 100% rename from src/endpoint/iam/ops/iam_get_access_key.js rename to src/endpoint/iam/ops/get_access_key_last_used.js diff --git a/src/endpoint/iam/ops/iam_delete_access_key.js b/src/endpoint/iam/ops/iam_delete_access_key.js index bb0a5609f5..f200144b5c 100644 --- a/src/endpoint/iam/ops/iam_delete_access_key.js +++ b/src/endpoint/iam/ops/iam_delete_access_key.js @@ -10,7 +10,7 @@ const { CONTENT_TYPE_APP_FORM_URLENCODED } = require('../../../util/http_utils') async function delete_access_key(req, res) { const params = { - user_name: req.body.user_name, + username: req.body.user_name, access_key: req.body.access_key_id }; dbg.log1('IAM DELETE ACCESS KEY', params); diff --git a/src/endpoint/iam/ops/iam_update_access_key.js b/src/endpoint/iam/ops/iam_update_access_key.js index b34c78d051..429e5c6c2c 100644 --- a/src/endpoint/iam/ops/iam_update_access_key.js +++ b/src/endpoint/iam/ops/iam_update_access_key.js @@ -12,7 +12,7 @@ async function update_access_key(req, res) { const params = { access_key: req.body.access_key_id, status: req.body.status, - user_name: req.body.user_name, + username: req.body.user_name, }; dbg.log1('IAM UPDATE ACCESS KEY', params); await req.account_sdk.update_access_key(params); diff --git a/src/endpoint/s3/s3_errors.js b/src/endpoint/s3/s3_errors.js index 9d49e27e70..82446ab2fe 100644 --- a/src/endpoint/s3/s3_errors.js +++ b/src/endpoint/s3/s3_errors.js @@ -599,6 +599,7 @@ S3Error.RPC_ERRORS_TO_S3 = Object.freeze({ INVALID_REQUEST: S3Error.InvalidRequest, NOT_IMPLEMENTED: S3Error.NotImplemented, INVALID_ACCESS_KEY_ID: S3Error.InvalidAccessKeyId, + DEACTIVATED_ACCESS_KEY_ID: S3Error.InvalidAccessKeyId, SIGNATURE_DOES_NOT_MATCH: S3Error.SignatureDoesNotMatch, SERVICE_UNAVAILABLE: S3Error.ServiceUnavailable, INVALID_RANGE: S3Error.InvalidRange, diff --git a/src/endpoint/sts/sts_rest.js b/src/endpoint/sts/sts_rest.js index 7835c5f002..402d0ea233 100644 --- a/src/endpoint/sts/sts_rest.js +++ b/src/endpoint/sts/sts_rest.js @@ -19,6 +19,7 @@ const RPC_ERRORS_TO_STS = Object.freeze({ SIGNATURE_DOES_NOT_MATCH: StsError.AccessDeniedException, UNAUTHORIZED: StsError.AccessDeniedException, INVALID_ACCESS_KEY_ID: StsError.AccessDeniedException, + DEACTIVATED_ACCESS_KEY_ID: StsError.AccessDeniedException, NO_SUCH_ACCOUNT: StsError.AccessDeniedException, NO_SUCH_ROLE: StsError.AccessDeniedException }); diff --git a/src/manage_nsfs/nc_master_key_manager.js b/src/manage_nsfs/nc_master_key_manager.js index 89fbdc2e4f..dff0760de6 100644 --- a/src/manage_nsfs/nc_master_key_manager.js +++ b/src/manage_nsfs/nc_master_key_manager.js @@ -327,10 +327,14 @@ class NCMasterKeysManager { async encrypt_access_keys(account) { await this.init(); const master_key_id = this.active_master_key.id; - const encrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({ - access_key: access_keys.access_key, - encrypted_secret_key: await this.encrypt(access_keys.secret_key, master_key_id) - }))); + const encrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => { + const encrypted_access_keys_object = { ...access_keys }; + if (encrypted_access_keys_object.secret_key) { + encrypted_access_keys_object.encrypted_secret_key = await this.encrypt(access_keys.secret_key, master_key_id); + delete encrypted_access_keys_object.secret_key; + } + return encrypted_access_keys_object; + })); return { ...account, access_keys: encrypted_access_keys, master_key_id }; } @@ -340,13 +344,22 @@ class NCMasterKeysManager { * @returns {Promise} */ async decrypt_access_keys(account) { - const decrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({ - access_key: access_keys.access_key, - secret_key: await this.decrypt(access_keys.encrypted_secret_key, account.master_key_id) - }))); + const decrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => { + const decrypted_access_keys_object = { ...access_keys }; + if (decrypted_access_keys_object.encrypted_secret_key) { + decrypted_access_keys_object.secret_key = await this.decrypt(access_keys.encrypted_secret_key, account.master_key_id); + delete decrypted_access_keys_object.encrypted_secret_key; + } + return decrypted_access_keys_object; + })); return decrypted_access_keys; } + async get_active_master_key_id() { + await this.init(); + return this.active_master_key.id; + } + /** * _validate_master_key_manager validates the master keys before decrypt/encrypt * @param {nb.ID} master_key_id diff --git a/src/sdk/account_sdk.js b/src/sdk/account_sdk.js index 0a7944fda2..ad62bfc602 100644 --- a/src/sdk/account_sdk.js +++ b/src/sdk/account_sdk.js @@ -78,7 +78,7 @@ class AccountSDK { const token = this.get_auth_token(); // If the request is signed (authenticated) if (token) { - signature_utils.authorize_request_account_by_token(token, this.requesting_account, false); + signature_utils.authorize_request_account_by_token(token, this.requesting_account); return; } throw new RpcError('UNAUTHORIZED', `No permission to access`); diff --git a/src/sdk/accountspace_fs.js b/src/sdk/accountspace_fs.js index 3b054d1bf8..d7db101383 100644 --- a/src/sdk/accountspace_fs.js +++ b/src/sdk/accountspace_fs.js @@ -10,65 +10,29 @@ const nb_native = require('../util/nb_native'); const native_fs_utils = require('../util/native_fs_utils'); const { CONFIG_SUBDIRS } = require('../manage_nsfs/manage_nsfs_constants'); const { create_arn, IAM_DEFAULT_PATH, get_action_message_title, - check_iam_path_was_set } = require('../endpoint/iam/iam_utils'); -const { generate_id } = require('../manage_nsfs/manage_nsfs_cli_utils'); + check_iam_path_was_set, MAX_NUMBER_OF_ACCESS_KEYS, + access_key_status_enum, identity_enum } = require('../endpoint/iam/iam_utils'); const nsfs_schema_utils = require('../manage_nsfs/nsfs_schema_utils'); const IamError = require('../endpoint/iam/iam_errors').IamError; - -const access_key_status_enum = { - ACTIVE: 'Active', - INACTIVE: 'Inactive', -}; - -const entity_enum = { +const cloud_utils = require('../util/cloud_utils'); +const SensitiveString = require('../util/sensitive_string'); +const { get_symlink_config_file_path, get_config_file_path, get_config_data, + generate_id } = require('../manage_nsfs/manage_nsfs_cli_utils'); +const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance(); +const { account_cache } = require('./object_sdk'); + +const entity_enum = Object.freeze({ USER: 'USER', ACCESS_KEY: 'ACCESS_KEY', -}; +}); + +// TODO - rename (the typo), move and reuse in manage_nsfs +const acounts_dir_relative_path = '../accounts/'; //////////////////// // MOCK VARIABLES // //////////////////// /* mock variables (until we implement the actual code), based on the example in AWS IAM API docs*/ - -// account_id should be taken from the root user (account._id in the config file); -const dummy_account_id = '12345678012'; // for the example -// user_id should be taken from config file of the new created user user (account._id in the config file); -const dummy_user_id = '12345678013'; // for the example -// user should be from the the config file and the details (this for the example) -const dummy_iam_path = '/division_abc/subdivision_xyz/'; -const dummy_username1 = 'Bob'; -const dummy_username2 = 'Robert'; -const dummy_username_requester = 'Alice'; -const dummy_user1 = { - username: dummy_username1, - user_id: dummy_user_id, - iam_path: dummy_iam_path, -}; -// the requester at current implementation is the root user (this is for the example) -const dummy_requester = { - username: dummy_username_requester, - user_id: dummy_account_id, - iam_path: IAM_DEFAULT_PATH, -}; -const MS_PER_MINUTE = 60 * 1000; -const dummy_access_key1 = { - username: dummy_username1, - access_key: 'AKIAIOSFODNN7EXAMPLE', - status: access_key_status_enum.ACTIVE, - secret_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLE', -}; -const dummy_access_key2 = { - username: dummy_username2, - access_key: 'CMCTDRBIDNN9EXAMPLE', - status: access_key_status_enum.ACTIVE, - secret_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLE', -}; -const dummy_requester_access_key = { - username: dummy_username_requester, - access_key: 'BLYDNFMRUCIS8EXAMPLE', - status: access_key_status_enum.ACTIVE, - secret_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLE', -}; const dummy_region = 'us-west-2'; const dummy_service_name = 's3'; @@ -80,11 +44,11 @@ class AccountSpaceFS { * @param {{ * config_root?: string; * fs_root?: string; - * fs_backend?: string; + * config_root_backend?: string; * stats?: import('./endpoint_stats_collector').EndpointStatsCollector; * }} params */ - constructor({ config_root, fs_root, fs_backend, stats }) { + constructor({ config_root, fs_root, config_root_backend, stats }) { this.config_root = config_root; this.accounts_dir = path.join(config_root, CONFIG_SUBDIRS.ACCOUNTS); this.access_keys_dir = path.join(config_root, CONFIG_SUBDIRS.ACCESS_KEYS); @@ -93,7 +57,7 @@ class AccountSpaceFS { // Currently we do not use these properties this.fs_root = fs_root ?? ''; - this.fs_backend = fs_backend ?? config.NSFS_NC_CONFIG_DIR_BACKEND; + this.config_root_backend = config_root_backend ?? config.NSFS_NC_CONFIG_DIR_BACKEND; this.stats = stats; } @@ -128,23 +92,26 @@ 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 - // 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 + // 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 async get_user(params, account_sdk) { const action = 'get_user'; dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); try { const requesting_account = account_sdk.requesting_account; + const { requester } = this._check_root_account_or_user(requesting_account, params.username); + const username = params.username ?? requester.name; // username is not required // GAP - we do not have the user iam_path at this point (error message) this._check_if_requesting_account_is_root_account(action, requesting_account, - { username: params.username }); - 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_get = await native_fs_utils.read_file(this.fs_context, account_config_path); + { username: username }); + 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_user_is_owned_by_root_account(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, iam_path: account_to_get.iam_path || IAM_DEFAULT_PATH, @@ -161,13 +128,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 + // 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 // 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 // 7 - (else not an update of username) update the config file + // 8 - remove the access_keys from the account_cache async update_user(params, account_sdk) { const action = 'update_user'; try { @@ -178,27 +146,30 @@ class AccountSpaceFS { { username: params.username}); 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_update = await native_fs_utils.read_file(this.fs_context, account_config_path); - this._check_if_requested_account_is_root_account(action, requesting_account, account_to_update, params); - this._check_if_user_is_owned_by_root_account(action, requesting_account, account_to_update); + 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_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) && params.new_username !== params.username; - if (!_.isUndefined(params.new_iam_path)) account_to_update.iam_path = params.new_iam_path; + if (!_.isUndefined(params.new_iam_path)) requested_account.iam_path = params.new_iam_path; if (is_username_update) { dbg.log1(`AccountSpaceFS.${action} username was updated, is_username_update`, is_username_update); - await this._update_account_config_new_username(action, params, account_to_update); + await this._update_account_config_new_username(action, params, requested_account); } else { - const account_to_update_string = JSON.stringify(account_to_update); - nsfs_schema_utils.validate_account_schema(JSON.parse(account_to_update_string)); + const requested_account_encrypted = await nc_mkm.encrypt_access_keys(requested_account); + const account_string = JSON.stringify(requested_account_encrypted); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); await native_fs_utils.update_config_file(this.fs_context, this.accounts_dir, - account_config_path, account_to_update_string); + account_config_path, account_string); } + this._clean_account_cache(requested_account); return { - iam_path: account_to_update.iam_path || IAM_DEFAULT_PATH, - username: account_to_update.name, - user_id: account_to_update._id, - arn: create_arn(requesting_account._id, account_to_update.name, account_to_update.iam_path), + iam_path: requested_account.iam_path || IAM_DEFAULT_PATH, + username: requested_account.name, + user_id: requested_account._id, + arn: create_arn(requesting_account._id, requested_account.name, requested_account.iam_path), }; } catch (err) { dbg.error(`AccountSpaceFS.${action} error`, err); @@ -208,7 +179,7 @@ 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 + // 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) @@ -224,9 +195,9 @@ class AccountSpaceFS { { username: params.username }); 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 native_fs_utils.read_file(this.fs_context, 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_user_is_owned_by_root_account(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 native_fs_utils.delete_config_file(this.fs_context, this.accounts_dir, account_config_path); } catch (err) { @@ -259,73 +230,233 @@ class AccountSpaceFS { // ACCESS KEY // //////////////// + // 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 + // 5 - check that the number of access key array + // 6 - generate access keys + // 7 - encryption + // 8 - validate account + // 9 - update account config file + // 10 - link new access key file to config file async create_access_key(params, account_sdk) { - const { dummy_access_key } = get_user_details(params.username); - dbg.log1('create_access_key', params); - return { - username: dummy_access_key.username, - access_key: dummy_access_key.access_key, - status: dummy_access_key.status, - secret_key: dummy_access_key.secret_key, - create_date: new Date(), - }; + const action = 'create_access_key'; + dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); + try { + const requesting_account = account_sdk.requesting_account; + const requester = this._check_if_requesting_account_is_root_account_or_user_om_himself(action, + requesting_account, params.username); + const name_for_access_key = params.username ?? requester.name; + const requested_account_config_path = this._get_account_config_path(name_for_access_key); + await this._check_if_account_config_file_exists(action, name_for_access_key, requested_account_config_path); + 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); + } + this._check_number_of_access_key_array(action, requested_account); + const { generated_access_key, generated_secret_key } = this._generate_access_key(); + const created_access_key_obj = { + access_key: generated_access_key, + secret_key: generated_secret_key, + creation_date: new Date().toISOString(), + deactivated: false, + }; + requested_account.access_keys.push(created_access_key_obj); + const requested_account_encrypted = await nc_mkm.encrypt_access_keys(requested_account); + const account_to_create_access_keys_string = JSON.stringify(requested_account_encrypted); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_to_create_access_keys_string)); + await native_fs_utils.update_config_file(this.fs_context, this.accounts_dir, + requested_account_config_path, account_to_create_access_keys_string); + await this._create_access_key_symlink(requested_account.name, generated_access_key); + return { + username: requested_account.name, + access_key: created_access_key_obj.access_key, + create_date: created_access_key_obj.creation_date, + status: this._get_access_key_status(created_access_key_obj.deactivated), + secret_key: generated_secret_key, + }; + } catch (err) { + dbg.error(`AccountSpaceFS.${action} error`, err); + throw this._translate_error_codes(err, entity_enum.ACCESS_KEY); + } } + // 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 + // General note: only serves the requester (no flag --user-name is passed) async get_access_key_last_used(params, account_sdk) { - dbg.log1('get_access_key_last_used', params); - return { - region: dummy_region, - last_used_date: new Date(Date.now() - 30 * MS_PER_MINUTE), - service_name: dummy_service_name, - username: dummy_user1.username, - }; + const action = 'get_access_key_last_used'; + dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); + try { + 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); + 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); + return { + region: dummy_region, // GAP + last_used_date: new Date(), // GAP + service_name: dummy_service_name, // GAP + username: requested_account.name, + }; + } catch (err) { + dbg.error('AccountSpaceFS.get_access_key_last_used error', err); + throw this._translate_error_codes(err, entity_enum.ACCESS_KEY); + } } + // 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 async update_access_key(params, account_sdk) { - dbg.log1('update_access_key', params); - // nothing to do at this point + const action = 'update_access_key'; + dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); + try { + const requesting_account = account_sdk.requesting_account; + const access_key_id = params.access_key; + 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); + const requested_account = await this._get_account_decrypted_data_optional(requested_account_path, true); + this._check_if_requested_account_same_root_account_as_requesting_account(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 + // we can change this behavior - a matter of decision + dbg.log1(`AccountSpaceFS.${action} status was not change, not updating the account config file`); + return; + } + access_key_obj.deactivated = this._check_access_key_is_deactivated(params.status); + const requested_account_encrypted = await nc_mkm.encrypt_access_keys(requested_account); + const account_string = JSON.stringify(requested_account_encrypted); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); + const name_for_access_key = params.username ?? requester.name; + const requested_account_config_path = this._get_account_config_path(name_for_access_key); + await native_fs_utils.update_config_file(this.fs_context, this.accounts_dir, + requested_account_config_path, account_string); + this._clean_account_cache(requested_account); + } catch (err) { + dbg.error(`AccountSpaceFS.${action} error`, err); + throw this._translate_error_codes(err, entity_enum.ACCESS_KEY); + } } + // 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 async delete_access_key(params, account_sdk) { - dbg.log1('delete_access_key', params); - // nothing to do at this point + const action = 'delete_access_key'; + dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); + try { + const requesting_account = account_sdk.requesting_account; + const access_key_id = params.access_key; + 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); + const requested_account = await this._get_account_decrypted_data_optional(requested_account_path, true); + this._check_if_requested_account_same_root_account_as_requesting_account(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); + const account_string = JSON.stringify(requested_account_encrypted); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); + const name_for_access_key = params.username ?? requester.name; + const account_config_path = this._get_account_config_path(name_for_access_key); + await native_fs_utils.update_config_file(this.fs_context, this.accounts_dir, + account_config_path, account_string); + await nb_native().fs.unlink(this.fs_context, requested_account_path); + this._clean_account_cache(requested_account); + } catch (err) { + dbg.error(`AccountSpaceFS.${action} error`, err); + throw this._translate_error_codes(err, entity_enum.ACCESS_KEY); + } } + // 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 + // 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?) async list_access_keys(params, account_sdk) { - dbg.log1('list_access_keys', params); - const is_truncated = false; - const { dummy_user } = get_user_details(params.username); - const username = dummy_user.username; - // iam_path_prefix is not supported in the example - const members = [{ - username: dummy_access_key1.username, - access_key: dummy_access_key1.access_key, - status: dummy_access_key1.status, - create_date: new Date(Date.now() - 30 * MS_PER_MINUTE), - }, - { - username: dummy_access_key2.username, - access_key: dummy_access_key2.access_key, - status: dummy_access_key2.status, - create_date: new Date(Date.now() - 30 * MS_PER_MINUTE), - }, - ]; - return { members, is_truncated, username }; + const action = 'list_access_keys'; + dbg.log1(`AccountSpaceFS.${action}`, params, account_sdk); + try { + const requesting_account = account_sdk.requesting_account; + const requester = this._check_if_requesting_account_is_root_account_or_user_om_himself(action, + requesting_account, params.username); + const name_for_access_key = params.username ?? requester.name; + const requested_account_config_path = this._get_account_config_path(name_for_access_key); + await this._check_if_account_config_file_exists(action, name_for_access_key, requested_account_config_path); + 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); + 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)); + return { members, is_truncated, username: name_for_access_key }; + } catch (err) { + dbg.error(`AccountSpaceFS.${action} error`, err); + throw this._translate_error_codes(err, entity_enum.ACCESS_KEY); + } } //////////////////////// // INTERNAL FUNCTIONS // //////////////////////// + // this function was copied from namespace_fs and bucketspace_fs + // It is a fallback that we use, but might be not accurate + _translate_error_codes(err, entity) { + if (err.rpc_code) return err; + if (err.code === 'ENOENT') err.rpc_code = `NO_SUCH_${entity}`; + if (err.code === 'EEXIST') err.rpc_code = `${entity}_ALREADY_EXISTS`; + if (err.code === 'EPERM' || err.code === 'EACCES') err.rpc_code = 'UNAUTHORIZED'; + if (err.code === 'IO_STREAM_ITEM_TIMEOUT') err.rpc_code = 'IO_STREAM_ITEM_TIMEOUT'; + if (err.code === 'INTERNAL_ERROR') err.rpc_code = 'INTERNAL_ERROR'; + return err; + } + _get_account_config_path(name) { - return path.join(this.accounts_dir, name + '.json'); + return get_config_file_path(this.accounts_dir, name); } _get_access_keys_config_path(access_key) { - return path.join(this.access_keys_dir, access_key + '.symlink'); + return get_symlink_config_file_path(this.access_keys_dir, access_key); + } + + async _get_account_decrypted_data_optional(account_path, should_decrypt_secret_key) { + const data = await get_config_data(this.config_root_backend, account_path, true); + if (should_decrypt_secret_key) data.access_keys = await nc_mkm.decrypt_access_keys(data); + return data; } - _new_user_defaults(requesting_account, params) { + _new_user_defaults(requesting_account, params, master_key_id) { const distinguished_name = requesting_account.nsfs_account_config.distinguished_name; return { _id: generate_id(), @@ -335,7 +466,7 @@ class AccountSpaceFS { owner: requesting_account._id, creator: requesting_account._id, iam_path: params.iam_path || IAM_DEFAULT_PATH, - master_key_id: requesting_account.master_key_id, // doesn't have meaning when user has just created (without access keys), TODO: tke from current master key manage and not just copy from the root account + master_key_id: master_key_id, allow_bucket_creation: requesting_account.allow_bucket_creation, force_md5_etag: requesting_account.force_md5_etag, access_keys: [], @@ -349,20 +480,8 @@ class AccountSpaceFS { }; } - // this function was copied from namespace_fs and bucketspace_fs - // It is a fallback that we use, but might be not accurate - _translate_error_codes(err, entity) { - if (err.rpc_code) return err; - if (err.code === 'ENOENT') err.rpc_code = `NO_SUCH_${entity}`; - if (err.code === 'EEXIST') err.rpc_code = `${entity}_ALREADY_EXISTS`; - if (err.code === 'EPERM' || err.code === 'EACCES') err.rpc_code = 'UNAUTHORIZED'; - if (err.code === 'IO_STREAM_ITEM_TIMEOUT') err.rpc_code = 'IO_STREAM_ITEM_TIMEOUT'; - if (err.code === 'INTERNAL_ERROR') err.rpc_code = 'INTERNAL_ERROR'; - return err; - } - _check_root_account(account) { - if (_.isUndefined(account.owner) || + if (account.owner === undefined || account.owner === account._id) { return true; } @@ -370,19 +489,44 @@ class AccountSpaceFS { } _check_root_account_owns_user(root_account, user_account) { - if (_.isUndefined(user_account.owner)) return false; + if (user_account.owner === undefined) return false; return root_account._id === user_account.owner; } - _throw_access_denied_error(action, requesting_account, user_details = {}) { - const arn_for_requesting_account = create_arn(requesting_account._id, - requesting_account.name.unwrap(), requesting_account.iam_path); - const arn_for_user = create_arn(requesting_account._id, user_details.username, user_details.iam_path); + // TODO: move to IamError class with a template + _throw_access_denied_error(action, requesting_account, details, entity) { const full_action_name = get_action_message_title(action); - const message_with_details = `User: ${arn_for_requesting_account} is not authorized to perform:` + - `${full_action_name} on resource: ` + - `${arn_for_user} because no identity-based policy allows the ${full_action_name} action`; - const { code, http_code, type } = IamError.AccessDenied; + const arn_for_requesting_account = create_arn(requesting_account._id, + requesting_account.name.unwrap(), requesting_account.path); + const basic_message = `User: ${arn_for_requesting_account} is not authorized to perform:` + + `${full_action_name} on resource: `; + let message_with_details; + if (entity === entity_enum.USER) { + let user_message; + if (action === 'list_access_keys') { + user_message = `user ${requesting_account.name.unwrap()}`; + } else { + user_message = create_arn(requesting_account._id, details.username, details.path); + } + message_with_details = basic_message + + `${user_message} because no identity-based policy allows the ${full_action_name} action`; + } else { // entity_enum.ACCESS_KEY + message_with_details = basic_message + `access key ${details.access_key}`; + } + const { code, http_code, type } = IamError.AccessDeniedException; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + + // TODO: move to IamError class with a template + _throw_error_perform_action_on_another_root_account(action, requesting_account, requested_account) { + const username = requested_account.name instanceof SensitiveString ? + requested_account.name.unwrap() : requested_account.name; + // we do not want to to reveal that the root account exists (or usernames under it) + // (cannot perform action on users from another root accounts) + dbg.error(`AccountSpaceFS.${action} root account of requested account is different than requesting root account`, + requesting_account, requested_account); + const message_with_details = `The user with name ${username} cannot be found.`; + const { code, http_code, type } = IamError.NoSuchEntity; throw new IamError({ code, message: message_with_details, http_code, type }); } @@ -394,11 +538,11 @@ class AccountSpaceFS { const config_files_list = 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 native_fs_utils.read_file(this.fs_context, full_path); + 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)) { if (should_filter_by_prefix) { - if (_.isUndefined(account_data.iam_path)) return undefined; + if (account_data.iam_path === undefined) return undefined; if (!account_data.iam_path.startsWith(iam_path_prefix)) return undefined; } const user_data = { @@ -425,35 +569,34 @@ class AccountSpaceFS { if (!is_root_account) { dbg.error(`AccountSpaceFS.${action} requesting account is not a root account`, requesting_account); - this._throw_access_denied_error(action, requesting_account, user_details); + this._throw_access_denied_error(action, requesting_account, user_details, entity_enum.USER); } } - _check_if_requested_account_is_root_account(action, requesting_account, requested_account, user_details = {}) { + _check_if_requested_account_is_root_account(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) { - dbg.error(`AccountSpaceFS.${action} requested account is a root account`, - requested_account); - this._throw_access_denied_error(action, requesting_account, user_details); + this._throw_error_perform_action_on_another_root_account(action, requesting_account, requested_account); } } async _check_username_already_exists(action, username) { - const account_config_path = this._get_account_config_path(username); - const name_exists = await native_fs_utils.is_path_exists(this.fs_context, - account_config_path); - if (name_exists) { - dbg.error(`AccountSpaceFS.${action} username already exists`, username); - const message_with_details = `User with name ${username} already exists.`; - const { code, http_code, type } = IamError.EntityAlreadyExists; - throw new IamError({ code, message: message_with_details, http_code, type }); - } + const account_config_path = this._get_account_config_path(username); + const name_exists = await native_fs_utils.is_path_exists(this.fs_context, + account_config_path); + if (name_exists) { + dbg.error(`AccountSpaceFS.${action} username already exists`, username); + const message_with_details = `User with name ${username} already exists.`; + const { code, http_code, type } = IamError.EntityAlreadyExists; + throw new IamError({ code, message: message_with_details, http_code, type }); + } } async _copy_data_from_requesting_account_to_account_config(action, requesting_account, params) { - const created_account = this._new_user_defaults(requesting_account, params); + const master_key_id = await nc_mkm.get_active_master_key_id(); + const created_account = this._new_user_defaults(requesting_account, params, master_key_id); dbg.log1(`AccountSpaceFS.${action} new_account`, created_account); const new_account_string = JSON.stringify(created_account); nsfs_schema_utils.validate_account_schema(JSON.parse(new_account_string)); @@ -474,7 +617,7 @@ class AccountSpaceFS { } } - _check_if_user_is_owned_by_root_account(action, requesting_account, requested_account) { + _check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account) { 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`, @@ -485,7 +628,6 @@ class AccountSpaceFS { } } - _check_if_user_does_not_have_access_keys_before_deletion(action, account_to_delete) { const is_access_keys_removed = account_to_delete.access_keys.length === 0; if (!is_access_keys_removed) { @@ -497,43 +639,158 @@ class AccountSpaceFS { } } - async _update_account_config_new_username(action, params, account_to_update) { + async _create_access_key_symlink(requested_account_name, access_key_id) { + const account_config_relative_path = get_config_file_path(acounts_dir_relative_path, requested_account_name); + const new_access_key_symlink_config_path = get_symlink_config_file_path(this.access_keys_dir, access_key_id); + await nb_native().fs.symlink(this.fs_context, account_config_relative_path, new_access_key_symlink_config_path); + } + + async _update_account_config_new_username(action, params, requested_account) { await this._check_username_already_exists(action, params.new_username); - account_to_update.name = params.new_username; - account_to_update.email = params.new_username; // internally saved - const account_to_update_string = JSON.stringify(account_to_update); - nsfs_schema_utils.validate_account_schema(JSON.parse(account_to_update_string)); + // prepare + requested_account.name = params.new_username; + requested_account.email = params.new_username; // internally saved + const access_key_ids = []; + for (const access_keys of requested_account.access_keys) { + access_key_ids.push(access_keys.access_key); + } + // handle account config creation + const requested_account_encrypted = await nc_mkm.encrypt_access_keys(requested_account); + const account_string = JSON.stringify(requested_account_encrypted); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); const new_username_account_config_path = this._get_account_config_path(params.new_username); await native_fs_utils.create_config_file(this.fs_context, this.accounts_dir, - new_username_account_config_path, account_to_update_string); + new_username_account_config_path, account_string); + // handle access keys (unlink and then create the new symbolic link) + for (const access_key_id of access_key_ids) { + const requested_account_path = get_symlink_config_file_path(this.access_keys_dir, access_key_id); + await nb_native().fs.unlink(this.fs_context, requested_account_path); + this._create_access_key_symlink(params.new_username, access_key_id); + } + // handle account config deletion const account_config_path = this._get_account_config_path(params.username); - await native_fs_utils.delete_config_file(this.fs_context, this.accounts_dir, - account_config_path); + await native_fs_utils.delete_config_file(this.fs_context, this.accounts_dir, account_config_path); } -} -////////////////////// -// HELPER FUNCTIONS // -////////////////////// + _check_root_account_or_user(requesting_account, username) { + let is_root_account_or_user_on_itself = false; + let requester = {}; + const requesting_account_name = requesting_account.name instanceof SensitiveString ? + requesting_account.name.unwrap() : requesting_account.name; + // root account (on user or himself) + if (this._check_root_account(requesting_account)) { + requester = { + name: requesting_account_name, + identity: identity_enum.ROOT_ACCOUNT + }; + is_root_account_or_user_on_itself = true; + return { is_root_account_or_user_on_itself, requester}; + } + // user (on himself) - username can be undefined + if (username === undefined || requesting_account_name === username) { + const username_to_use = username ?? requesting_account_name; + requester = { + name: username_to_use, + identity: identity_enum.USER + }; + is_root_account_or_user_on_itself = true; + return { is_root_account_or_user_on_itself, requester }; + } + return { is_root_account_or_user_on_itself, requester }; + } -/** - * get_user_details will return the relevant details of the user since username is not required in some requests - * (If it is not included, it defaults to the user making the request). - * If the username is passed in the request than it is this user - * else (undefined) is is the requester - * @param {string|undefined} username - */ -function get_user_details(username) { - const res = { - dummy_user: dummy_requester, - dummy_access_key: dummy_requester_access_key, - }; - const is_user_request = Boolean(username); // can be user request or root user request - if (is_user_request) { - res.dummy_user = dummy_user1; - res.dummy_access_key = dummy_access_key1; - } - return res; + // TODO reuse set_access_keys from manage_nsfs + _generate_access_key() { + let generated_access_key; + let generated_secret_key; + ({ access_key: generated_access_key, secret_key: generated_secret_key } = cloud_utils.generate_access_keys()); + generated_access_key = generated_access_key.unwrap(); + generated_secret_key = generated_secret_key.unwrap(); + return { generated_access_key, generated_secret_key}; + } + + _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) { + return true; + } + } + return false; + } + + _get_access_key_status(deactivated) { + // we would like the default to be Active (so when it is undefined it would be Active) + const status = deactivated ? access_key_status_enum.INACTIVE : access_key_status_enum.ACTIVE; + return status; + } + + _check_access_key_is_deactivated(status) { + return status === access_key_status_enum.INACTIVE; + } + + _list_access_keys_from_account(account) { + const members = []; + for (const access_key of account.access_keys) { + const member = { + username: account.name, + access_key: access_key.access_key, + status: this._get_access_key_status(access_key.deactivated), + create_date: access_key.creation_date ?? account.creation_date, + }; + members.push(member); + } + return members; + } + + _check_if_requesting_account_is_root_account_or_user_om_himself(action, requesting_account, username) { + const { is_root_account_or_user_on_itself, requester } = this._check_root_account_or_user( + requesting_account, + username + ); + dbg.log1(`AccountSpaceFS.${action} requesting_account`, requesting_account, + 'is_root_account_or_user_on_itself', is_root_account_or_user_on_itself); + if (!is_root_account_or_user_on_itself) { + dbg.error(`AccountSpaceFS.${action} requesting account is neither a root account ` + + `nor user requester on himself`, + requesting_account); + this._throw_access_denied_error(action, requesting_account, { username }, entity_enum.USER); + } + return requester; + } + + _check_number_of_access_key_array(action, requested_account) { + if (requested_account.access_keys.length >= MAX_NUMBER_OF_ACCESS_KEYS) { + dbg.error(`AccountSpaceFS.${action} requested account is not owned by root account `, + requested_account); + const message_with_details = `Cannot exceed quota for AccessKeysPerUser: ${MAX_NUMBER_OF_ACCESS_KEYS}.`; + const { code, http_code, type } = IamError.LimitExceeded; + throw new IamError({ code, message: message_with_details, http_code, type }); + } + } + + async _check_if_account_exists_by_access_key_symlink(action, requesting_account, account_path, access_key) { + 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); + } + } + + _check_if_requested_account_same_root_account_as_requesting_account(action, requesting_account, requested_account) { + 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) { + this._throw_error_perform_action_on_another_root_account(action, requesting_account, requested_account); + } + } + + // 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) { + for (const access_keys of requested_account.access_keys) { + const access_key_id = access_keys.access_key; + account_cache.invalidate_key(access_key_id); + } + } } // EXPORTS diff --git a/src/sdk/object_sdk.js b/src/sdk/object_sdk.js index bd1aaca062..0fd299b1ad 100644 --- a/src/sdk/object_sdk.js +++ b/src/sdk/object_sdk.js @@ -243,7 +243,7 @@ class ObjectSDK { const token = this.get_auth_token(); // If the request is signed (authenticated) if (token) { - signature_utils.authorize_request_account_by_token(token, this.requesting_account, true); + signature_utils.authorize_request_account_by_token(token, this.requesting_account); } // check for a specific bucket if (bucket && req.op_name !== 'put_bucket') { diff --git a/src/sdk/sts_sdk.js b/src/sdk/sts_sdk.js index 0a748dd34b..14fb98150a 100644 --- a/src/sdk/sts_sdk.js +++ b/src/sdk/sts_sdk.js @@ -86,7 +86,7 @@ class StsSDK { const token = this.get_auth_token(); // If the request is signed (authenticated) if (token) { - signature_utils.authorize_request_account_by_token(token, this.requesting_account, false); + signature_utils.authorize_request_account_by_token(token, this.requesting_account); return; } throw new RpcError('UNAUTHORIZED', `No permission to access bucket`); diff --git a/src/server/system_services/schemas/nsfs_account_schema.js b/src/server/system_services/schemas/nsfs_account_schema.js index 197b37be37..87b6a297f0 100644 --- a/src/server/system_services/schemas/nsfs_account_schema.js +++ b/src/server/system_services/schemas/nsfs_account_schema.js @@ -60,6 +60,12 @@ module.exports = { encrypted_secret_key: { type: 'string', }, + creation_date: { + type: 'string', + }, + deactivated: { + type: 'boolean', + }, } } }, 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 0bca93f55b..915b7cedd3 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 @@ -15,7 +15,7 @@ const SensitiveString = require('../../../util/sensitive_string'); const AccountSpaceFS = require('../../../sdk/accountspace_fs'); const { TMP_PATH } = require('../../system_tests/test_utils'); const { get_process_fs_context } = require('../../../util/native_fs_utils'); -const { IAM_DEFAULT_PATH } = require('../../../endpoint/iam/iam_utils'); +const { IAM_DEFAULT_PATH, access_key_status_enum } = require('../../../endpoint/iam/iam_utils'); 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(); @@ -93,6 +93,26 @@ function make_dummy_account_sdk_non_root_user() { return account_sdk; } +function make_dummy_account_sdk_iam_user(account, root_account_id) { + return { + requesting_account: { + _id: account._id, + name: new SensitiveString(account.name), + email: new SensitiveString(account.email), + creation_date: account.creation_date, + access_keys: [{ + access_key: new SensitiveString(account.access_keys[0].access_key), + secret_key: new SensitiveString(account.access_keys[0].secret_key) + }], + nsfs_account_config: account.nsfs_account_config, + allow_bucket_creation: account.allow_bucket_creation, + master_key_id: account.master_key_id, + owner: root_account_id, + creator: root_account_id, + }, + }; +} + // 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() { @@ -123,7 +143,7 @@ describe('Accountspace_FS tests', () => { 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); - for (const account of [root_user_account]) { + for (const account of [root_user_account, root_user_account2]) { 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); @@ -142,6 +162,8 @@ describe('Accountspace_FS tests', () => { const dummy_iam_path2 = '/division_def/subdivision_uvw/'; const dummy_username1 = 'Bob'; const dummy_username2 = 'Robert'; + const dummy_username3 = 'Alice'; + const dummy_username4 = 'James'; const dummy_user1 = { username: dummy_username1, iam_path: dummy_iam_path, @@ -185,7 +207,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); @@ -232,7 +254,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); @@ -246,11 +268,11 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); - it('get_user should return an error if user account does not exists', async function() { + it('get_user should return an error if user account does not exist', async function() { try { const params = { username: 'non-existing-user', @@ -277,6 +299,31 @@ describe('Accountspace_FS tests', () => { expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); + + it('get_user should return user params (user has access_keys)', async function() { + const account_sdk = make_dummy_account_sdk(); + // create the user + let params = { + username: dummy_username4, + }; + await accountspace_fs.create_user(params, account_sdk); + // create the access key + params = { + username: dummy_username4, + }; + await accountspace_fs.create_access_key(params, account_sdk); + // get the user + params = { + username: dummy_username4, + }; + 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_username4); + expect(res.arn).toBeDefined(); + expect(res.create_date).toBeDefined(); + expect(res.password_last_used).toBeDefined(); + }); }); describe('update_user', () => { @@ -328,7 +375,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); @@ -342,11 +389,11 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); - it('update_user should return an error if user account does not exists', async function() { + it('update_user should return an error if user account does not exist', async function() { try { const params = { username: 'non-existing-user', @@ -416,6 +463,38 @@ describe('Accountspace_FS tests', () => { }; await accountspace_fs.update_user(params, account_sdk); }); + + it('update_user should return user params (user has access_keys)', async function() { + const account_sdk = make_dummy_account_sdk(); + // create the user + let params = { + username: dummy_username3, + }; + await accountspace_fs.create_user(params, account_sdk); + // create the access key + params = { + username: dummy_username3, + }; + const res_access_key_creation = await accountspace_fs.create_access_key(params, account_sdk); + const access_key = res_access_key_creation.access_key; + // rename the user + const dummy_new_username = dummy_username3 + '-new-user-name'; + params = { + username: dummy_username3, + new_username: dummy_new_username, + }; + const res = await accountspace_fs.update_user(params, account_sdk); + expect(res.iam_path).toBe(IAM_DEFAULT_PATH); + expect(res.username).toBe(params.new_username); + expect(res.user_id).toBeDefined(); + expect(res.arn).toBeDefined(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, params.new_username); + expect(user_account_config_file.name).toBe(params.new_username); + const symlink_config_path = path.join(accountspace_fs.access_keys_dir, access_key + '.symlink'); + await fs_utils.file_must_exist(symlink_config_path); + 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.new_username); + }); }); describe('delete_user', () => { @@ -426,6 +505,8 @@ describe('Accountspace_FS tests', () => { const account_sdk = make_dummy_account_sdk(); 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() { @@ -438,7 +519,7 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); @@ -452,11 +533,11 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); } }); - it('delete_user should return an error if user account does not exists', async function() { + it('delete_user should return an error if user account does not exist', async function() { try { const params = { username: 'non-existing-user', @@ -484,8 +565,23 @@ describe('Accountspace_FS tests', () => { } }); - it.skip('delete_user should return an error if user has access keys', async function() { - // TODO after implementing create_access_key + it('delete_user should return an error if user has access keys', async function() { + const params = { + username: dummy_user2.username, + }; + try { + const account_sdk = make_dummy_account_sdk(); + // create the access key + // same params + await accountspace_fs.create_access_key(params, account_sdk); + await accountspace_fs.delete_user(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.DeleteConflict.code); + const user_account_config_path = path.join(accountspace_fs.accounts_dir, params.username + '.json'); + await fs_utils.file_must_exist(user_account_config_path); + } }); }); @@ -543,72 +639,689 @@ describe('Accountspace_FS tests', () => { throw new NoErrorThrownError(); } catch (err) { expect(err).toBeInstanceOf(IamError); - expect(err).toHaveProperty('code', IamError.AccessDenied.code); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); } }); }); }); describe('Accountspace_FS Access Keys tests', () => { + const dummy_path = '/division_abc/subdivision_xyz/'; const dummy_username1 = 'Bob'; + const dummy_username2 = 'Robert'; + const dummy_username3 = 'Alice'; + const dummy_username4 = 'James'; + const dummy_username5 = 'Oliver'; + const dummy_username6 = 'Henry'; + const dummy_user1 = { + username: dummy_username1, + path: dummy_path, + }; + const dummy_user2 = { + username: dummy_username2, + path: dummy_path, + }; + const dummy_user3 = { + username: dummy_username3, + path: dummy_path, + }; + 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); + + for (const account of [root_user_account]) { + 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); + await fs.promises.writeFile(account_path, JSON.stringify(account)); + await fs.promises.chmod(account_path, 0o600); + await fs.promises.symlink(account_path, account_access_path); + } + }); + afterAll(async () => { + fs_utils.folder_delete(config_root); + fs_utils.folder_delete(new_buckets_path1); + }); + describe('create_access_key', () => { - it('create_access_key should return user access key params', async function() { + it('create_access_key should return an error if requesting user is not a root account user', async function() { + try { + const params = { + username: dummy_user1.username, + path: dummy_user1.path, + }; + const account_sdk = make_dummy_account_sdk_non_root_user(); + await accountspace_fs.create_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('create_access_key should return an error if user account does not exist', async function() { + try { + const params = { + username: 'non-existing-user', + }; + const account_sdk = make_dummy_account_sdk(); + await accountspace_fs.create_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('create_access_key should return an error if user is not owned by the root account', async function() { + try { + const params = { + username: dummy_user1.username, + }; + const account_sdk = make_dummy_account_sdk_not_for_creating_resources(); + await accountspace_fs.create_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('create_access_key should return user access key params (first time)', async function() { + const account_sdk = make_dummy_account_sdk(); + // create the user + const params_for_user_creation = { + username: dummy_user1.username, + path: dummy_user1.path, + }; + await accountspace_fs.create_user(params_for_user_creation, account_sdk); + // create the access key const params = { username: dummy_username1, }; + const res = await accountspace_fs.create_access_key(params, account_sdk); + expect(res.username).toBe(dummy_username1); + 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 user access key params (second time)', async function() { const account_sdk = make_dummy_account_sdk(); + // user was already created + // create the access key + const params = { + username: dummy_username1, + }; const res = await accountspace_fs.create_access_key(params, account_sdk); expect(res.username).toBe(dummy_username1); 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(2); + + 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); + expect(user_account_config_file_from_symlink.access_keys.length).toBe(2); + }); + + it('create_access_key should return an error if user already has 2 access keys', async function() { + try { + const account_sdk = make_dummy_account_sdk(); + // user was already created + // create the access key + 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.LimitExceeded.code); + } + }); + + it('create_access_key should return user access key params (requester is an IAM user)', async function() { + let account_sdk = make_dummy_account_sdk(); + // create the user + const params_for_user_creation = { + username: dummy_username5, + }; + await accountspace_fs.create_user(params_for_user_creation, account_sdk); + // create the first access key by the root account + const params_for_access_key_creation = { + username: dummy_username5, + }; + await accountspace_fs.create_access_key(params_for_access_key_creation, account_sdk); + 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); + const params = {}; + const res = await accountspace_fs.create_access_key(params, account_sdk); + expect(res.username).toBe(dummy_username5); + expect(res.access_key).toBeDefined(); + expect(res.status).toBe('Active'); + expect(res.secret_key).toBeDefined(); + + user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); + expect(user_account_config_file.name).toBe(dummy_username5); + 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(2); + + 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(dummy_username5); + expect(user_account_config_file_from_symlink.access_keys).toBeDefined(); + expect(Array.isArray(user_account_config_file_from_symlink.access_keys)).toBe(true); + expect(user_account_config_file_from_symlink.access_keys.length).toBe(2); + }); + + it('create_access_key should return an error if user is not owned by the root account (requester is an IAM user)', async function() { + try { + // both IAM users are under the same root account (owner property) + let account_sdk = make_dummy_account_sdk(); + 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); + const params = { + username: dummy_user1.username, + }; + await accountspace_fs.create_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.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() { + const account_sdk = make_dummy_account_sdk(); + // create the user + const params_for_user_creation = { + username: dummy_user2.username, + path: dummy_user2.path, + }; + await accountspace_fs.create_user(params_for_user_creation, account_sdk); + // create the access key + const params_for_access_key_creation = { + username: dummy_user2.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 = { - username: dummy_username1, + access_key: dummy_access_key, }; - const dummy_region = 'us-west-2'; - const account_sdk = make_dummy_account_sdk(); 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(dummy_username1); + expect(res.username).toBe(dummy_user2.username); }); + + it('get_access_key_last_used should return an error if access key do not exist', async function() { + try { + const dummy_access_key = 'AKIAIOSFODNN7EXAMPLE'; + const account_sdk = make_dummy_account_sdk(); + // user was already created + const params = { + access_key: dummy_access_key, + }; + await accountspace_fs.get_access_key_last_used(params, account_sdk); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('get_access_key_last_used should return an error if access key exists in another root account', async function() { + try { + const account_sdk = make_dummy_account_sdk(); + // create the user + const params_for_user_creation = { + username: dummy_user3.username, + path: dummy_user3.path, + }; + await accountspace_fs.create_user(params_for_user_creation, account_sdk); + // create the access key + const params_for_access_key_creation = { + username: dummy_user3.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 - by another root account + const account_sdk2 = make_dummy_account_sdk_not_for_creating_resources(); + const params = { + access_key: dummy_access_key, + }; + await accountspace_fs.get_access_key_last_used(params, account_sdk2); + } 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 (requester is an IAM user)', async function() { + 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); + const access_key = user_account_config_file.access_keys[1].access_key; + const params = { + access_key: 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(user_account_config_file.name); + }); + + // 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 }); describe('update_access_key', () => { - it('update_access_key does not return any param', async function() { + it('update_access_key should return an error if requesting user is not a root account user', async function() { + const dummy_access_key = 'pHqFNglDiq7eA0Q4XETq'; + try { + const params = { + username: dummy_username1, + access_key: dummy_access_key, + status: access_key_status_enum.ACTIVE, + }; + const account_sdk = make_dummy_account_sdk_non_root_user(); + await accountspace_fs.update_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('update_access_key should return an error if access key does not exist', async function() { + const dummy_access_key = 'pHqFNglDiq7eA0Q4XETq'; + try { + const params = { + username: dummy_username1, + access_key: dummy_access_key, + status: access_key_status_enum.ACTIVE, + }; + const account_sdk = make_dummy_account_sdk(); + await accountspace_fs.update_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('update_access_key should not return an error if access key is on another root account', async function() { + try { + const account_sdk = make_dummy_account_sdk_not_for_creating_resources(); + const 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; + const params = { + username: dummy_username1, + access_key: access_key, + status: access_key_status_enum.INACTIVE, + }; + await accountspace_fs.update_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('update_access_key should not return any param (update status to Inactive)', 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; const params = { username: dummy_username1, + 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, dummy_username1); + 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() { 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; + const params = { + username: dummy_username1, + access_key: access_key, + status: access_key_status_enum.ACTIVE, + }; + 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, dummy_username1); + 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() { + 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; + const params = { + username: dummy_username1, + access_key: access_key, + status: access_key_status_enum.ACTIVE, + }; 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, dummy_username1); + expect(user_account_config_file.access_keys[0].deactivated).toBe(false); + }); + + it('update_access_key should not return any param (requester is an IAM user)', async function() { + const dummy_username = dummy_username5; + 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); + const access_key = user_account_config_file.access_keys[1].access_key; + const params = { + 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, dummy_username); + expect(user_account_config_file.access_keys[1].deactivated).toBe(true); + }); + + it('update_access_key should return an error if user is not owned by the root account (requester is an IAM user)', async function() { + try { + // both IAM users are under the same root account (owner property) + let account_sdk = make_dummy_account_sdk(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); + 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); + const params = { + username: dummy_user1.username, + access_key: access_key, + status: access_key_status_enum.INACTIVE, + }; + await accountspace_fs.update_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } }); }); describe('delete_access_key', () => { - it('delete_access_key does not return any params', async function() { + it('delete_access_key should return an error if requesting user is not a root account user', async function() { + const dummy_access_key = 'pHqFNglDiq7eA0Q4XETq'; + try { + const params = { + username: dummy_username1, + access_key: dummy_access_key, + }; + const account_sdk = make_dummy_account_sdk_non_root_user(); + await accountspace_fs.delete_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('delete_access_key should return an error if access key does not exist', async function() { + const dummy_access_key = 'pHqFNglDiq7eA0Q4XETq'; + try { + const params = { + username: dummy_username1, + access_key: dummy_access_key, + }; + const account_sdk = make_dummy_account_sdk(); + await accountspace_fs.delete_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } + }); + + it('delete_access_key should not return an error if access key is on another root account', async function() { + try { + const account_sdk = make_dummy_account_sdk_not_for_creating_resources(); + const 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; + const params = { + username: dummy_username1, + access_key: access_key, + }; + await accountspace_fs.delete_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('delete_access_key should not return any param', 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; const params = { username: dummy_username1, + 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, dummy_username1); + 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); + }); + + it('delete_access_key should not return any param (account with 2 access keys)', async function() { + const username = dummy_username6; const account_sdk = make_dummy_account_sdk(); + // create the user + let params = { + username: username, + }; + await accountspace_fs.create_user(params, account_sdk); + // create the access key (first time) + params = { + username: username, + }; + // create the access key (second time) + const access_creation = await accountspace_fs.create_access_key(params, account_sdk); + const access_key_to_delete = access_creation.access_key; + // create the access key (second time) + const access_creation2 = await accountspace_fs.create_access_key(params, account_sdk); + const access_key = access_creation2.access_key; + params = { + username: username, + access_key: access_key_to_delete, + }; + const res = await accountspace_fs.delete_access_key(params, account_sdk); + expect(res).toBeUndefined(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, username); + expect(user_account_config_file.access_keys.length).toBe(1); + expect(user_account_config_file.access_keys[0].access_key).toBe(access_key); + expect(user_account_config_file.access_keys[0].access_key).not.toBe(access_key_to_delete); + let symlink_config_path = path.join(accountspace_fs.access_keys_dir, access_key_to_delete + '.symlink'); + await fs_utils.file_must_not_exist(symlink_config_path); + symlink_config_path = path.join(accountspace_fs.access_keys_dir, access_key + '.symlink'); + await fs_utils.file_must_exist(symlink_config_path); + }); + + it('delete_access_key should not return any param (requester is an IAM user)', async function() { + 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); + const access_key = user_account_config_file.access_keys[1].access_key; + const params = { + 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, dummy_username1); + expect(user_account_config_file.access_keys.length).toBe(1); + expect(user_account_config_file.access_keys[0].access_key).not.toBe(access_key); + const symlink_config_path = path.join(accountspace_fs.access_keys_dir, access_key + '.symlink'); + await fs_utils.file_must_not_exist(symlink_config_path); + }); + + it('delete_access_key should return an error if user is not owned by the root account (requester is an IAM user)', async function() { + try { + // both IAM users are under the same root account (owner property) + let account_sdk = make_dummy_account_sdk(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); + 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); + const params = { + username: dummy_user1.username, + access_key: access_key, + }; + await accountspace_fs.delete_access_key(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } }); }); describe('list_access_keys', () => { - it('list_access_keys return array of users and value of is_truncated', async function() { - const params = {}; + it('list_access_keys return array of access_keys and value of is_truncated', async function() { + const params = { + username: dummy_username1, + }; const account_sdk = make_dummy_account_sdk(); - const res = await accountspace_fs.list_users(params, account_sdk); + 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', dummy_username1); + expect(res.members[0].access_key).toBeDefined(); + expect(res.members[0].status).toBeDefined(); + expect(res.members[0].create_date).toBeDefined(); + }); + + it('list_access_keys should return an error when username does not exists', async function() { + const non_exsting_user = 'non_exsting_user'; + try { + const params = { + username: non_exsting_user, + }; + const account_sdk = make_dummy_account_sdk(); + await accountspace_fs.list_access_keys(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('list_access_keys should return an error when username does not exists (in the root account)', async function() { + try { + const params = { + username: dummy_username1, + }; + const account_sdk = make_dummy_account_sdk_not_for_creating_resources(); + await accountspace_fs.list_access_keys(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.NoSuchEntity.code); + } + }); + + it('list_access_keys return empty array of access_keys is user does not have access_keys', async function() { + const account_sdk = make_dummy_account_sdk(); + // create the user + const params_for_user_creation = { + username: dummy_username4, + }; + await accountspace_fs.create_user(params_for_user_creation, account_sdk); + // list the access_keys + const params = { + username: dummy_username4, + }; + 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(0); + }); + + it('list_access_keys return array of access_keys and value of is_truncated (requester is an IAM user)', async function() { + 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); + const params = {}; + 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); + }); + + it('list_access_keys should return an error if user is not owned by the root account (requester is an IAM user)', async function() { + try { + // both IAM users are under the same root account (owner property) + let account_sdk = make_dummy_account_sdk(); + const user_account_config_file = await read_config_file(accountspace_fs.accounts_dir, dummy_username5); + 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); + const params = { + username: dummy_user1.username, + access_key: access_key, + }; + await accountspace_fs.list_access_keys(params, account_sdk); + throw new NoErrorThrownError(); + } catch (err) { + expect(err).toBeInstanceOf(IamError); + expect(err).toHaveProperty('code', IamError.AccessDeniedException.code); + } }); }); }); diff --git a/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js b/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js index 62b2c7b555..6fb6177d2b 100644 --- a/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js @@ -10,6 +10,7 @@ const nb_native = require('../../../util/nb_native'); const cloud_utils = require('../../../util/cloud_utils'); const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager'); const { get_process_fs_context } = require('../../../util/native_fs_utils'); +const nsfs_schema_utils = require('../../../manage_nsfs/nsfs_schema_utils'); const DEFAULT_FS_CONFIG = get_process_fs_context(); const MASTER_KEYS_JSON_PATH = path.join(config.NSFS_NC_DEFAULT_CONF_DIR, 'master_keys.json'); @@ -61,6 +62,47 @@ describe('NC master key manager tests - file store type', () => { const decrypted_secret_key = await nc_mkm_instance.decrypt(encrypted_secret_key, nc_mkm_instance.active_master_key.id); expect(unwrapped_secret_key).toEqual(decrypted_secret_key); }); + + it('encrypt_access_keys of account (with extra properties)', async () => { + const master_key_id = nc_mkm_instance.active_master_key.id; + const account = get_account_data(master_key_id); + account.access_keys[0] = get_access_key_object(); + account.access_keys[1] = get_access_key_object(); + const account_after_encryption = await nc_mkm_instance.encrypt_access_keys(account); + const account_string = JSON.stringify(account_after_encryption); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); + expect(account_after_encryption._id).toBe(account._id); + expect(account_after_encryption.name).toBe(account.name); + expect(account_after_encryption.creation_date).toBe(account.creation_date); + expect(account_after_encryption.allow_bucket_creation).toBe(account.allow_bucket_creation); + expect(account_after_encryption.master_key_id).toBeDefined(); + expect(account_after_encryption.nsfs_account_config.uid).toBe(account.nsfs_account_config.uid); + expect(account_after_encryption.nsfs_account_config.gid).toBe(account.nsfs_account_config.gid); + expect(account_after_encryption.access_keys.length).toBe(account.access_keys.length); + for (let i = 0; i < account_after_encryption.access_keys.length; i++) { + expect(account_after_encryption.access_keys[i].access_key).toBe(account.access_keys[i].access_key); + expect(account_after_encryption.access_keys[i].encrypted_secret_key).toBeDefined(); // instead of secret_key + expect(account_after_encryption.access_keys[i].creation_date).toBe(account.access_keys[i].creation_date); + expect(account_after_encryption.access_keys[i].deactivated).toBe(account.access_keys[i].deactivated); + } + }); + + it('decrypt_access_keys of account (with extra properties)', async () => { + const master_key_id = nc_mkm_instance.active_master_key.id; + const account = get_account_data(master_key_id); + account.access_keys[0] = get_access_key_object(); + account.access_keys[1] = get_access_key_object(); + const account_after_encryption = await nc_mkm_instance.encrypt_access_keys(account); + const account_string = JSON.stringify(account_after_encryption); + nsfs_schema_utils.validate_account_schema(JSON.parse(account_string)); + const decrypted_access_keys = await nc_mkm_instance.decrypt_access_keys(account_after_encryption); + for (let i = 0; i < decrypted_access_keys.length; i++) { + expect(decrypted_access_keys[i].access_key).toBe(account.access_keys[i].access_key); + expect(decrypted_access_keys[i].secret_key).toBeDefined(); // instead of encrypted_secret_key + expect(decrypted_access_keys[i].creation_date).toBe(account.access_keys[i].creation_date); + expect(decrypted_access_keys[i].deactivated).toBe(account.access_keys[i].deactivated); + } + }); }); it('should fail - init nc_mkm - invalid master_keys.json - missing active_master_key', async () => { @@ -156,3 +198,42 @@ async function read_master_keys_json() { function fail(reason) { throw new Error(reason); } + +// copied from account validation (with changes) +function get_account_data(master_key_id) { + const account_name = 'account1'; + const id = '65a62e22ceae5e5f1a758aa9'; + const account_email = account_name; // temp, keep the email internally + const creation_date = new Date('December 17, 2023 09:00:00').toISOString(); + const nsfs_account_config_uid_gid = { + uid: 1001, + gid: 1001, + }; + + const account_data = { + _id: id, + name: account_name, + email: account_email, + master_key_id: master_key_id, + access_keys: [], // no access-keys + nsfs_account_config: { + ...nsfs_account_config_uid_gid + }, + creation_date: creation_date, + allow_bucket_creation: true, + }; + + return account_data; +} + +function get_access_key_object() { + const { access_key, secret_key } = cloud_utils.generate_access_keys(); + const unwrapped_secret_key = secret_key.unwrap(); + const access_key_object = { + access_key: access_key, + secret_key: unwrapped_secret_key, + creation_date: '2024-06-03T07:40:58.808Z', + deactivated: false, + }; + return access_key_object; +} 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 62da74db3b..01da4d3ba6 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,19 @@ describe('schema validation NC NSFS account', () => { 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(); + nsfs_schema_utils.validate_account_schema(account_data); + }); + + it('account with access_key object (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(); + account_data.access_keys.splice(0, 1); // this will remove index 0 item and move the index 1 item to its place + nsfs_schema_utils.validate_account_schema(account_data); + }); + }); describe('account with additional properties', () => { @@ -414,6 +427,16 @@ describe('schema validation NC NSFS account', () => { const message = 'must be string'; 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(); + account_data.access_keys[0] = undefined; // Should use account_data.access_keys.splice(0, 1) to avoid this failure + const reason = 'Test should have failed because the access_key array in index 0 is undefined'; + const message = 'must be object | {"type":"object"} | "/access_keys/0"'; + assert_validation(account_data, reason, message); + }); + }); describe('skip schema check by config test', () => { @@ -461,6 +484,18 @@ function get_account_data() { return account_data; } +function get_access_key_with_additional_properties() { + const access_key_id = 'GIGiFAnjaaE7OKD5N7hA'; + const encrypted_secret_key = 'Wn2ctcgGHxdTh7L0OMjv3Ym30jHAPECjJD/B7LfRAmWuuN8RUbTgSw==EXAMPLE'; + const access_keys_object = { + access_key: access_key_id, + encrypted_secret_key: encrypted_secret_key, + creation_date: '2024-06-03T07:40:58.808Z', + deactivated: false, + }; + return access_keys_object; +} + function assert_validation(account_to_validate, reason, basic_message) { try { nsfs_schema_utils.validate_account_schema(account_to_validate); diff --git a/src/util/signature_utils.js b/src/util/signature_utils.js index 8d2a9d6c49..785075d9b4 100644 --- a/src/util/signature_utils.js +++ b/src/util/signature_utils.js @@ -362,13 +362,47 @@ function authenticate_request_by_service(req, sdk) { } // mutual code of S3, STS and IAM services -function authorize_request_account_by_token(token, requesting_account, is_secret_optional) { - const signature_secret = token.temp_secret_key || requesting_account?.access_keys?.[0]?.secret_key?.unwrap(); - const should_check_signature = !is_secret_optional || signature_secret; - if (should_check_signature) { - const signature = get_signature_from_auth_token(token, signature_secret); - if (token.signature !== signature) throw new RpcError('SIGNATURE_DOES_NOT_MATCH', `Signature that was calculated did not match`); +// 1 - check access key status (in case deactivated property true) +// 2 - check secret_key signature (in case we should check the signature) +function authorize_request_account_by_token(token, requesting_account) { + if (!requesting_account) _throw_error_authorize_request_account_by_token('requesting_account'); + if (!token) _throw_error_authorize_request_account_by_token('token'); + + const access_key_id_to_find = token.access_key; + const access_key_obj = _.find(requesting_account.access_keys, + item => item.access_key.unwrap() === access_key_id_to_find); + + if (!access_key_obj) { + // we should never get here + dbg.error(`authorize_request_account_by_token: could not find access_key_id ${access_key_id_to_find}`); + throw new RpcError('INVALID_ACCESS_KEY_ID', `Account with access_key not found`); } + + if (access_key_obj.deactivated) { // if it is undefined then it is Active by default + dbg.error(`authorize_request_account_by_token: access key id ${access_key_id_to_find} ` + + `status is Inactive`); + throw new RpcError('DEACTIVATED_ACCESS_KEY_ID', `Account with access_key deactivated`); + } + + if (!access_key_obj.secret_key) { + // Should we never get here? + // (with a question mark since we had optional chaining) + _throw_error_authorize_request_account_by_token('secret_key'); + } + + const signature_secret = token.temp_secret_key || access_key_obj.secret_key.unwrap(); + const signature = get_signature_from_auth_token(token, signature_secret); + if (token.signature !== signature) { + throw new RpcError('SIGNATURE_DOES_NOT_MATCH', `Signature that was calculated did not match`); + } +} + +/** + * @param {string} component + */ +function _throw_error_authorize_request_account_by_token(component) { + dbg.error(`authorize_request_account_by_token: we don't have ${component}`); + throw new RpcError('INTERNAL_ERROR', `Needs ${component} for authorize request account`); }