diff --git a/src/endpoint/endpoint.js b/src/endpoint/endpoint.js index f57ac2c7a3..316d22d285 100755 --- a/src/endpoint/endpoint.js +++ b/src/endpoint/endpoint.js @@ -24,6 +24,7 @@ const endpoint_utils = require('./endpoint_utils'); const StsSDK = require('../sdk/sts_sdk'); const ObjectIO = require('../sdk/object_io'); const ObjectSDK = require('../sdk/object_sdk'); +const NBAccountSDK = require('../sdk/nb_account_sdk'); const xml_utils = require('../util/xml_utils'); const http_utils = require('../util/http_utils'); const net_utils = require('../util/net_utils'); @@ -44,6 +45,7 @@ const { get_notification_logger } = require('../util/notifications_util'); const ldap_client = require('../util/ldap_client'); const { is_nc_environment } = require('../nc/nc_utils'); const NoobaaEvent = require('../manage_nsfs/manage_nsfs_events_utils').NoobaaEvent; + const cluster = /** @type {import('node:cluster').Cluster} */ ( /** @type {unknown} */ (require('node:cluster')) @@ -431,6 +433,11 @@ function create_init_request_sdk(rpc, internal_rpc_client, object_io) { object_io, stats: endpoint_stats_collector.instance(), }); + req.account_sdk = new NBAccountSDK({ + rpc_client, + internal_rpc_client, + //stats: endpoint_stats_collector.instance(), + }); }; return init_request_sdk; } diff --git a/src/endpoint/iam/iam_constants.js b/src/endpoint/iam/iam_constants.js index 54adae6614..f2abff1a5b 100644 --- a/src/endpoint/iam/iam_constants.js +++ b/src/endpoint/iam/iam_constants.js @@ -58,6 +58,7 @@ const IAM_PARAMETER_NAME = Object.freeze({ NEW_USERNAME: 'NewUserName', }); +const IAM_SPLIT_CHARACTERS = ':'; // EXPORTS exports.IAM_ACTIONS = IAM_ACTIONS; @@ -70,3 +71,4 @@ exports.IAM_DEFAULT_PATH = IAM_DEFAULT_PATH; exports.AWS_NOT_USED = AWS_NOT_USED; exports.IAM_SERVICE_SMALL_LETTERS = IAM_SERVICE_SMALL_LETTERS; exports.IAM_PARAMETER_NAME = IAM_PARAMETER_NAME; +exports.IAM_SPLIT_CHARACTERS = IAM_SPLIT_CHARACTERS; diff --git a/src/sdk/accountspace_nb.js b/src/sdk/accountspace_nb.js new file mode 100644 index 0000000000..f68d2c9a35 --- /dev/null +++ b/src/sdk/accountspace_nb.js @@ -0,0 +1,256 @@ +/* Copyright (C) 2024 NooBaa */ +'use strict'; + +const _ = require('lodash'); +const SensitiveString = require('../util/sensitive_string'); +const account_util = require('../util/account_util'); +const iam_utils = require('../endpoint/iam/iam_utils'); +const dbg = require('../util/debug_module')(__filename); +const system_store = require('..//server/system_services/system_store').get_instance(); +// const { account_cache } = require('./object_sdk'); +const IamError = require('../endpoint/iam/iam_errors').IamError; +const { IAM_ACTIONS, IAM_DEFAULT_PATH, IAM_SPLIT_CHARACTERS } = require('../endpoint/iam/iam_constants'); + + +/* + TODO: DISCUSS: + 1. IAM API only for account created using IAM API and OBC accounts not from admin, support, + operator and account created using noobaa. + 2. Do we need to have two access keys + 3. get_access_key_last_used() API call could return dummy values? +*/ + +/** + * @implements {nb.AccountSpace} + */ +class AccountSpaceNB { + /** + * @param {{ + * rpc_client: nb.APIClient; + * internal_rpc_client: nb.APIClient; + * stats?: import('./endpoint_stats_collector').EndpointStatsCollector; + * }} params + */ + constructor({ rpc_client, internal_rpc_client, stats }) { + this.rpc_client = rpc_client; + this.internal_rpc_client = internal_rpc_client; + this.stats = stats; + } + + ////////////////////// + // ACCOUNT METHODS // + ////////////////////// + + async create_user(params, account_sdk) { + + const action = IAM_ACTIONS.CREATE_USER; + const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email); + account_util._check_if_requesting_account_is_root_account(action, requesting_account, + { username: params.username, path: params.iam_path }); + account_util._check_username_already_exists(action, params, requesting_account); + const iam_arn = iam_utils.create_arn_for_user(requesting_account._id.toString(), params.username, params.iam_path); + const account_name = account_util.populate_username(params.username, requesting_account.name.unwrap()); + const req = { + rpc_params: { + name: account_name, + email: account_name, + has_login: false, + s3_access: true, + allow_bucket_creation: true, + owner: requesting_account._id.toString(), + is_iam: true, + iam_arn: iam_arn, + iam_path: params.iam_path, + role: 'iam_user', + + // TODO: default_resource remove + default_resource: 'noobaa-default-backing-store', + }, + account: requesting_account, + }; + // CORE CHANGES PENDING - START + const iam_account = await account_util.create_account(req); + // CORE CHANGES PENDING - END + + // TODO : Clean account cache + // TODO : Send Event + const requested_account = system_store.get_account_by_email(account_name); + return { + iam_path: requested_account.iam_path || IAM_DEFAULT_PATH, + username: params.username, + user_id: iam_account.id, + arn: iam_arn, + create_date: iam_account.create_date, + }; + + } + + async get_user(params, account_sdk) { + const action = IAM_ACTIONS.GET_USER; + const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email); + const account_name = account_util.populate_username(params.username, requesting_account.name.unwrap()); + const requested_account = system_store.get_account_by_email(account_name); + account_util._check_if_requesting_account_is_root_account(action, requesting_account, + { username: params.username, iam_path: params.iam_path }); + account_util._check_if_account_exists(action, account_name); + account_util._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + account_util._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account); + const reply = { + user_id: requested_account._id.toString(), + // TODO : IAM PATH + iam_path: requested_account.iam_path || IAM_DEFAULT_PATH, + username: account_util.get_iam_username(requested_account.name.unwrap()), + arn: requested_account.iam_arn, + // TODO: GAP Need to save created date + create_date: new Date(), + // TODO: Dates missing : GAP + password_last_used: new Date(), + }; + return reply; + } + + async update_user(params, account_sdk) { + const action = IAM_ACTIONS.UPDATE_USER; + const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email); + const username = account_util.populate_username(params.username, requesting_account.name.unwrap()); + account_util._check_if_requesting_account_is_root_account(action, requesting_account, + { username: params.username, iam_path: params.iam_path }); + account_util._check_if_account_exists(action, username); + const requested_account = system_store.get_account_by_email(username); + let iam_path = requested_account.iam_path; + let user_name = requested_account.name.unwrap(); + account_util._check_username_already_exists(action, { username: params.new_username }, requesting_account); + account_util._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + account_util._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account); + if (params.new_iam_path !== undefined) iam_path = params.new_iam_path; + if (params.new_username !== undefined) user_name = params.new_username; + const iam_arn = iam_utils.create_arn_for_user(requested_account._id.toString(), user_name, iam_path); + const new_account_name = new SensitiveString(`${params.new_username}:${requesting_account.name.unwrap()}`); + const updates = { + name: new_account_name, + email: new_account_name, + iam_arn: iam_arn, + iam_path: iam_path, + }; + // CORE CHANGES PENDING - START + await system_store.make_changes({ + update: { + accounts: [{ + _id: requested_account._id, + $set: _.omitBy(updates, _.isUndefined), + }] + } + }); + // CORE CHANGES PENDING - END + // TODO : Clean account cache + // TODO : Send Event + return { + // TODO: IAM path needs to be saved + iam_path: iam_path || IAM_DEFAULT_PATH, + username: user_name, + user_id: requested_account._id.toString(), + arn: iam_arn + }; + + } + + async delete_user(params, account_sdk) { + const action = IAM_ACTIONS.DELETE_USER; + // GAP - we do not have the user iam_path at this point (error message) + //const requesting_account = account_sdk.requesting_account; + const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email); + const username = account_util.populate_username(params.username, requesting_account.name.unwrap()); + account_util._check_if_requesting_account_is_root_account(action, requesting_account, { username: params.username }); + account_util._check_if_account_exists(action, username); + const requested_account = system_store.get_account_by_email(username); + account_util._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account); + //const root_account = system_store.get_account_by_email(requesting_account.email); + account_util._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account); + // TODO: DELETE INLINE POLICY : Manually + // TODO: DELETE ACCESS KEY : manually + const req = { + system: system_store.data.systems[0], + account: requested_account, + }; + // CORE CHANGES PENDING - START + return account_util.delete_account(req, requested_account); + // CORE CHANGES PENDING - END + // TODO : clean account cache + + } + + async list_users(params, account_sdk) { + const action = IAM_ACTIONS.LIST_USERS; + //const requesting_account = account_sdk.requesting_account; + const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email); + account_util._check_if_requesting_account_is_root_account(action, requesting_account, { }); + const is_truncated = false; // GAP - no pagination at this point + + const root_name = requesting_account.name.unwrap(); + // CORE CHANGES PENDING - START + const requesting_account_iam_users = _.filter(system_store.data.accounts, function(acc) { + if (!acc.name.unwrap().includes(IAM_SPLIT_CHARACTERS)) { + return false; + } + return acc.name.unwrap().split(IAM_SPLIT_CHARACTERS)[1] === root_name; + }); + let members = _.map(requesting_account_iam_users, function(iam_user) { + const member = { + user_id: iam_user._id.toString(), + iam_path: iam_user.iam_path || IAM_DEFAULT_PATH, + username: iam_user.name.unwrap().split(IAM_SPLIT_CHARACTERS)[0], + arn: iam_user.iam_arn, + // TODO: GAP Need to save created date + create_date: new Date(), + // TODO: GAP Miising password_last_used + password_last_used: Date.now(), // GAP + }; + return member; + }); + // CORE CHANGES PENDING - END + members = members.sort((a, b) => a.username.localeCompare(b.username)); + return { members, is_truncated }; + } + + ///////////////////////////////// + // ACCOUNT ACCESS KEY METHODS // + ///////////////////////////////// + + async create_access_key(params, account_sdk) { + // TODO + dbg.log0('AccountSpaceNB.create_access_key:', params); + const { code, http_code, type } = IamError.NotImplemented; + throw new IamError({ code, message: 'NotImplemented', http_code, type }); + } + + async get_access_key_last_used(params, account_sdk) { + dbg.log0('AccountSpaceNB.get_access_key_last_used:', params); + const { code, http_code, type } = IamError.NotImplemented; + throw new IamError({ code, message: 'NotImplemented', http_code, type }); + } + + async update_access_key(params, account_sdk) { + dbg.log0('AccountSpaceNB.update_access_key:', params); + const { code, http_code, type } = IamError.NotImplemented; + throw new IamError({ code, message: 'NotImplemented', http_code, type }); + } + + async delete_access_key(params, account_sdk) { + dbg.log0('AccountSpaceNB.delete_access_key:', params); + const { code, http_code, type } = IamError.NotImplemented; + throw new IamError({ code, message: 'NotImplemented', http_code, type }); + } + + async list_access_keys(params, account_sdk) { + dbg.log0('AccountSpaceNB.list_access_keys:', params); + const { code, http_code, type } = IamError.NotImplemented; + throw new IamError({ code, message: 'NotImplemented', http_code, type }); + } + + //////////////////// + // POLICY METHODS // + //////////////////// +} + +// EXPORTS +module.exports = AccountSpaceNB; diff --git a/src/sdk/nb_account_sdk.js b/src/sdk/nb_account_sdk.js new file mode 100644 index 0000000000..2114c1b721 --- /dev/null +++ b/src/sdk/nb_account_sdk.js @@ -0,0 +1,29 @@ +/* Copyright (C) 2016 NooBaa */ +'use strict'; + +const AccountSDK = require('../sdk/account_sdk'); +const BucketSpaceNB = require('./bucketspace_nb'); +const AccountSpaceNB = require('../sdk/accountspace_nb'); + +// NBAccountSDK was based on AccountSDK +class NBAccountSDK extends AccountSDK { + /** + * @param {{ + * rpc_client: nb.APIClient; + * internal_rpc_client: nb.APIClient; + * }} args + */ + constructor({rpc_client, internal_rpc_client}) { + const bucketspace = new BucketSpaceNB({ rpc_client, internal_rpc_client }); + const accountspace = new AccountSpaceNB({ rpc_client, internal_rpc_client }); + + super({ + rpc_client: rpc_client, + internal_rpc_client: internal_rpc_client, + bucketspace: bucketspace, + accountspace: accountspace, + }); + } +} + +module.exports = NBAccountSDK; diff --git a/src/server/system_services/account_server.js b/src/server/system_services/account_server.js index ead393c7d2..51c8e5fcb1 100644 --- a/src/server/system_services/account_server.js +++ b/src/server/system_services/account_server.js @@ -14,9 +14,7 @@ const { RpcError } = require('../../rpc'); const Dispatcher = require('../notifications/dispatcher'); const SensitiveString = require('../../util/sensitive_string'); const cloud_utils = require('../../util/cloud_utils'); -const auth_server = require('../common_services/auth_server'); const system_store = require('../system_services/system_store').get_instance(); -const pool_server = require('../system_services/pool_server'); const azure_storage = require('../../util/azure_storage_wrap'); const NetStorage = require('../../util/NetStorageKit-Node-master/lib/netstorage'); const usage_aggregator = require('../bg_services/usage_aggregator'); @@ -24,13 +22,9 @@ const { OP_NAME_TO_ACTION } = require('../../endpoint/sts/sts_rest'); const { Durations, LogsQueryClient } = require('@azure/monitor-query'); const { ClientSecretCredential } = require("@azure/identity"); const noobaa_s3_client = require('../../sdk/noobaa_s3_client/noobaa_s3_client'); +const account_util = require('./../../util/account_util'); -const demo_access_keys = Object.freeze({ - access_key: new SensitiveString('123'), - secret_key: new SensitiveString('abc') -}); - const check_connection_timeout = 15 * 1000; const check_new_azure_connection_timeout = 20 * 1000; @@ -40,132 +34,15 @@ const check_new_azure_connection_timeout = 20 * 1000; * */ async function create_account(req) { - const account = { - _id: ( - req.rpc_params.new_system_parameters ? - system_store.parse_system_store_id(req.rpc_params.new_system_parameters.account_id) : - system_store.new_system_store_id() - ), - name: req.rpc_params.name, - email: req.rpc_params.email, - has_login: req.rpc_params.has_login, - is_external: req.rpc_params.is_external, - nsfs_account_config: req.rpc_params.nsfs_account_config, - force_md5_etag: req.rpc_params.force_md5_etag - }; - - const { roles: account_roles = ['admin'] } = req.rpc_params; - - validate_create_account_permissions(req); - validate_create_account_params(req); - - if (account.name.unwrap() === 'demo' && account.email.unwrap() === 'demo@noobaa.com') { - account.access_keys = [demo_access_keys]; - } else { - const access_keys = req.rpc_params.access_keys || [cloud_utils.generate_access_keys()]; - if (!access_keys.length) throw new RpcError('FORBIDDEN', 'cannot create account without access_keys'); - account.access_keys = access_keys; - } - - if (req.rpc_params.must_change_password) { - account.next_password_change = new Date(); - } - - const sys_id = req.rpc_params.new_system_parameters ? - system_store.parse_system_store_id(req.rpc_params.new_system_parameters.new_system_id) : - req.system._id; - - if (req.rpc_params.s3_access) { - if (req.rpc_params.new_system_parameters) { - account.default_resource = system_store.parse_system_store_id(req.rpc_params.new_system_parameters.default_resource); - account.allow_bucket_creation = true; - } else { - // Default pool resource is backingstores - const resource = req.rpc_params.default_resource ? - req.system.pools_by_name[req.rpc_params.default_resource] || - (req.system.namespace_resources_by_name && req.system.namespace_resources_by_name[req.rpc_params.default_resource]) : - pool_server.get_default_pool(req.system); - if (!resource) throw new RpcError('BAD_REQUEST', 'default resource doesn\'t exist'); - if (resource.nsfs_config && resource.nsfs_config.fs_root_path && !req.rpc_params.nsfs_account_config) { - throw new RpcError('Invalid account configuration - must specify nsfs_account_config when default resource is a namespace resource'); - } - account.default_resource = resource._id; - account.allow_bucket_creation = _.isUndefined(req.rpc_params.allow_bucket_creation) ? - true : req.rpc_params.allow_bucket_creation; - - const bucket_claim_owner = req.rpc_params.bucket_claim_owner; - if (bucket_claim_owner) { - const creator_roles = req.account.roles_by_system[req.system._id]; - if (creator_roles.includes('operator')) { // Not allowed to create claim owner outside of the operator - account.bucket_claim_owner = req.system.buckets_by_name[bucket_claim_owner.unwrap()]._id; - } else { - dbg.warn('None operator user was trying to set a bucket-claim-owner for account', req.account); - } - } - } - } - - const roles = account_roles.map(role => ({ - _id: system_store.new_system_store_id(), - account: account._id, - system: sys_id, - role - })); - - // Suppress audit entry for creation of operator account. - if (!account_roles.includes('operator')) { - Dispatcher.instance().activity({ - event: 'account.create', - level: 'info', - system: (req.system && req.system._id) || sys_id, - actor: req.account && req.account._id, - account: account._id, - desc: `${account.email.unwrap()} was created ` + (req.account ? `by ${req.account.email.unwrap()}` : ``), - }); - } - const account_mkey = system_store.master_key_manager.new_master_key({ - description: `master key of ${account._id} account`, - cipher_type: system_store.data.systems[0].master_key_id.cipher_type, - master_key_id: system_store.data.systems[0].master_key_id._id - }); - account.master_key_id = account_mkey._id; - const decrypted_access_keys = _.cloneDeep(account.access_keys); - account.access_keys[0] = { - access_key: account.access_keys[0].access_key, - secret_key: system_store.master_key_manager.encrypt_sensitive_string_with_master_key_id( - account.access_keys[0].secret_key, account_mkey._id) - }; - if (req.rpc_params.role_config) { - validate_assume_role_policy(req.rpc_params.role_config.assume_role_policy); - account.role_config = req.rpc_params.role_config; - } - await system_store.make_changes({ - insert: { - accounts: [account], - roles, - master_keys: [account_mkey] - } - }); + account_util.validate_create_account_permissions(req); + account_util.validate_create_account_params(req); + const {token, access_keys} = await account_util.create_account(req); - const created_account = system_store.data.get_by_id(account._id); - const auth = { - account_id: created_account._id - }; - // since we created the first system for this account - // we expect just one system, but use _.each to get it from the map - const current_system = (req.system && req.system._id) || sys_id; - _.each(created_account.roles_by_system, (sys_roles, system_id) => { - //we cannot assume only one system. - if (current_system.toString() === system_id) { - auth.system_id = system_id; - auth.role = sys_roles[0]; - } - }); return { - token: auth_server.make_auth_token(auth), - access_keys: decrypted_access_keys + token, + access_keys }; } @@ -256,41 +133,7 @@ function read_account_by_access_key(req) { * */ async function generate_account_keys(req) { - const account = system_store.get_account_by_email(req.rpc_params.email); - if (!account) { - throw new RpcError('NO_SUCH_ACCOUNT', 'No such account email: ' + req.rpc_params.email); - } - if (req.system && req.account) { - if (!is_support_or_admin_or_me(req.system, req.account, account)) { - throw new RpcError('UNAUTHORIZED', 'Cannot update account'); - } - } - if (account.is_support) { - throw new RpcError('FORBIDDEN', 'Cannot update support account'); - } - const access_keys = cloud_utils.generate_access_keys(); - access_keys.secret_key = system_store.master_key_manager.encrypt_sensitive_string_with_master_key_id( - access_keys.secret_key, account.master_key_id._id); - - await system_store.make_changes({ - update: { - accounts: [{ - _id: account._id, - access_keys: [ - access_keys - ] - }] - } - }); - - Dispatcher.instance().activity({ - event: 'account.generate_credentials', - level: 'info', - system: req.system && req.system._id, - actor: req.account && req.account._id, - account: account._id, - desc: `Credentials for ${account.email.unwrap()} were regenerated ${req.account && 'by ' + req.account.email.unwrap()}`, - }); + return await account_util.generate_account_keys(req); } /** @@ -559,45 +402,7 @@ async function get_account_usage(req) { function delete_account(req) { const account_to_delete = system_store.get_account_by_email(req.rpc_params.email); _verify_can_delete_account(req, account_to_delete); - - const roles_to_delete = system_store.data.roles - .filter( - role => String(role.account._id) === String(account_to_delete._id) - ) - .map( - role => role._id - ); - - return system_store.make_changes({ - remove: { - accounts: [account_to_delete._id], - roles: roles_to_delete - } - }) - .then( - val => { - Dispatcher.instance().activity({ - event: 'account.delete', - level: 'info', - system: req.system && req.system._id, - actor: req.account && req.account._id, - account: account_to_delete._id, - desc: `${account_to_delete.email.unwrap()} was deleted by ${req.account && req.account.email.unwrap()}`, - }); - return val; - }, - err => { - Dispatcher.instance().activity({ - event: 'account.delete', - level: 'alert', - system: req.system && req.system._id, - actor: req.account && req.account._id, - account: account_to_delete._id, - desc: `Error: ${account_to_delete.email.unwrap()} failed to delete by ${req.account && req.account.email.unwrap()}`, - }); - throw err; - } - ); + return account_util.delete_account(req, account_to_delete); } @@ -1293,82 +1098,6 @@ function is_support_or_admin_or_me(system, account, target_account) { ); } -function validate_create_account_permissions(req) { - const account = req.account; - //For new system creation, nothing to be checked - if (req.rpc_params.new_system_parameters) return; - - //Only allow support, admin/operator roles and UI login enabled accounts to create new accounts - if (!account.is_support && - !account.has_login && - !(account.roles_by_system[req.system._id].some( - role => role === 'admin' || role === 'operator' - ))) { - throw new RpcError('UNAUTHORIZED', 'Cannot create new account'); - } -} - -function validate_create_account_params(req) { - // find none-internal pools - const has_non_internal_resources = (req.system && req.system.pools_by_name) ? - Object.values(req.system.pools_by_name).some(p => !p.is_default_pool) : - false; - - if (req.rpc_params.name.unwrap() !== req.rpc_params.name.unwrap().trim()) { - throw new RpcError('BAD_REQUEST', 'system name must not contain leading or trailing spaces'); - } - - if (system_store.get_account_by_email(req.rpc_params.email)) { - throw new RpcError('BAD_REQUEST', 'email address already registered'); - } - - if (req.rpc_params.s3_access) { - if (!req.rpc_params.new_system_parameters) { - if (req.system.pools_by_name === 0) { - throw new RpcError('No resources in the system - Can\'t create accounts'); - } - - if (req.rpc_params.allow_bucket_creation && !req.rpc_params.default_resource) { //default resource needed only if new bucket can be created - if (has_non_internal_resources) { // has resources which is not internal - must supply resource - throw new RpcError('BAD_REQUEST', 'Enabling S3 requires providing default_resource'); - } - } - } - - if (req.rpc_params.new_system_parameters) { - if (!req.rpc_params.new_system_parameters.default_resource) { - throw new RpcError( - 'BAD_REQUEST', - 'Creating new system with enabled S3 access for owner requires providing default_resource' - ); - } - } - } - - if (req.rpc_params.has_login) { - if (!req.rpc_params.password) { - throw new RpcError('BAD_REQUEST', 'Password is missing'); - } - - // Verify that account with login access have full s3 access permissions. - const { default_resource } = req.rpc_params.new_system_parameters || req.rpc_params; - const allow_bucket_creation = req.rpc_params.new_system_parameters ? - true : - req.rpc_params.allow_bucket_creation; - - if ( - !req.rpc_params.s3_access || - (has_non_internal_resources && !default_resource) || - !allow_bucket_creation - ) { - throw new RpcError('BAD_REQUEST', 'Accounts with login access must have full s3 access permissions'); - } - - } else if (req.rpc_params.password) { - throw new RpcError('BAD_REQUEST', 'Password should not be sent'); - } -} - async function verify_authorized_account(req) { //operator connects by token and doesn't have the password property. if (req.role === 'operator') { diff --git a/src/server/system_services/schemas/account_schema.js b/src/server/system_services/schemas/account_schema.js index d702c45720..b5424f074a 100644 --- a/src/server/system_services/schemas/account_schema.js +++ b/src/server/system_services/schemas/account_schema.js @@ -27,7 +27,12 @@ module.exports = { has_login: { type: 'boolean' }, password: { wrapper: SensitiveString }, // bcrypted password - DEPRECATED next_password_change: { date: true }, // DEPRECATED - + owner: { objectid: true }, + tagging: { + $ref: 'common_api#/definitions/tagging', + }, + iam_arn: { type: 'string' }, + iam_path: { type: 'string' }, // default policy for new buckets default_resource: { objectid: true }, default_chunk_config: { objectid: true }, diff --git a/src/server/system_services/schemas/role_schema.js b/src/server/system_services/schemas/role_schema.js index c6a9ec4df9..aebf1bf001 100644 --- a/src/server/system_services/schemas/role_schema.js +++ b/src/server/system_services/schemas/role_schema.js @@ -25,7 +25,7 @@ module.exports = { }, role: { type: 'string', - enum: ['admin', 'user', 'operator'] + enum: ['admin', 'user', 'operator', 'iam_user'] }, } }; diff --git a/src/server/system_services/schemas/system_schema.js b/src/server/system_services/schemas/system_schema.js index 52d26ade4b..2295a5cbf5 100644 --- a/src/server/system_services/schemas/system_schema.js +++ b/src/server/system_services/schemas/system_schema.js @@ -87,7 +87,8 @@ module.exports = { 'noobaa-db-pg-cluster-rw', 'noobaa-db-pg-cluster-ro', 'noobaa-db-pg-cluster-r', - 'noobaa-syslog' + 'noobaa-syslog', + 'iam' ] }, kind: { @@ -98,7 +99,7 @@ module.exports = { port: { $ref: 'common_api#/definitions/port' }, api: { type: 'string', - enum: ['mgmt', 's3', 'sts', 'md', 'bg', 'hosted_agents', 'mongodb', 'metrics', 'postgres', 'syslog'] + enum: ['mgmt', 's3', 'sts', 'md', 'bg', 'hosted_agents', 'mongodb', 'metrics', 'postgres', 'syslog', 'iam'] }, secure: { type: 'boolean' }, weight: { type: 'integer' } diff --git a/src/util/account_util.js b/src/util/account_util.js new file mode 100644 index 0000000000..97f7191ff6 --- /dev/null +++ b/src/util/account_util.js @@ -0,0 +1,569 @@ +/* Copyright (C) 2016 NooBaa */ +'use strict'; + +const _ = require('lodash'); +const Dispatcher = require('../server/notifications/dispatcher'); + +const dbg = require('../util/debug_module')(__filename); +const { RpcError } = require('../rpc'); +const SensitiveString = require('../util/sensitive_string'); +const cloud_utils = require('../util/cloud_utils'); +const auth_server = require('..//server/common_services/auth_server'); +const system_store = require('..//server/system_services/system_store').get_instance(); +const pool_server = require('../server/system_services/pool_server'); +const { OP_NAME_TO_ACTION } = require('../endpoint/sts/sts_rest'); +const IamError = require('../endpoint/iam/iam_errors').IamError; +//const { account_cache } = require('./../sdk/object_sdk'); +const { create_arn_for_user, get_action_message_title } = require('../endpoint/iam/iam_utils'); +const { IAM_ACTIONS, IAM_DEFAULT_PATH, IAM_SPLIT_CHARACTERS } = require('../endpoint/iam/iam_constants'); + +const demo_access_keys = Object.freeze({ + access_key: new SensitiveString('123'), + secret_key: new SensitiveString('abc') +}); +/** + * + * CREATE_ACCOUNT + * + */ +async function create_account(req) { + + const account = { + _id: ( + req.rpc_params.new_system_parameters ? + system_store.parse_system_store_id(req.rpc_params.new_system_parameters.account_id) : + system_store.new_system_store_id() + ), + name: req.rpc_params.name, + email: req.rpc_params.email, + has_login: req.rpc_params.has_login, + is_external: req.rpc_params.is_external, + nsfs_account_config: req.rpc_params.nsfs_account_config, + force_md5_etag: req.rpc_params.force_md5_etag + }; + + if (!req.system) { + req.system = system_store.data.systems[0]; + } + + const { roles: account_roles = ['admin'] } = req.rpc_params; + + if (req.rpc_params.must_change_password) { + account.next_password_change = new Date(); + } + + const sys_id = req.rpc_params.new_system_parameters ? + system_store.parse_system_store_id(req.rpc_params.new_system_parameters.new_system_id) : + req.system._id; + + if (req.rpc_params.s3_access) { + if (req.rpc_params.new_system_parameters) { + account.default_resource = system_store.parse_system_store_id(req.rpc_params.new_system_parameters.default_resource); + account.allow_bucket_creation = true; + } else { + // Default pool resource is backingstores + const resource = req.rpc_params.default_resource ? + req.system.pools_by_name[req.rpc_params.default_resource] || + (req.system.namespace_resources_by_name && req.system.namespace_resources_by_name[req.rpc_params.default_resource]) : + pool_server.get_default_pool(req.system); + if (!resource) throw new RpcError('BAD_REQUEST', 'default resource doesn\'t exist'); + if (resource.nsfs_config && resource.nsfs_config.fs_root_path && !req.rpc_params.nsfs_account_config) { + throw new RpcError('Invalid account configuration - must specify nsfs_account_config when default resource is a namespace resource'); + } + account.default_resource = resource._id; + account.allow_bucket_creation = _.isUndefined(req.rpc_params.allow_bucket_creation) ? + true : req.rpc_params.allow_bucket_creation; + + const bucket_claim_owner = req.rpc_params.bucket_claim_owner; + if (bucket_claim_owner) { + const creator_roles = req.account.roles_by_system[req.system._id]; + if (creator_roles.includes('operator')) { // Not allowed to create claim owner outside of the operator + account.bucket_claim_owner = req.system.buckets_by_name[bucket_claim_owner.unwrap()]._id; + } else { + dbg.warn('None operator user was trying to set a bucket-claim-owner for account', req.account); + } + } + } + } + + const roles = account_roles.map(role => ({ + _id: system_store.new_system_store_id(), + account: account._id, + system: sys_id, + role + })); + + // Suppress audit entry for creation of operator account. + if (!account_roles.includes('operator')) { + Dispatcher.instance().activity({ + event: 'account.create', + level: 'info', + system: (req.system && req.system._id) || sys_id, + actor: req.account && req.account._id, + account: account._id, + desc: `${account.email.unwrap()} was created ` + (req.account ? `by ${req.account.email.unwrap()}` : ``), + }); + } + const account_access_info = create_access_key_auth(req, account, req.rpc_params.is_iam); + + if (req.rpc_params.role_config) { + validate_assume_role_policy(req.rpc_params.role_config.assume_role_policy); + account.role_config = req.rpc_params.role_config; + } + // TODO : remove rpc_params + if (req.rpc_params.is_iam) { + account.owner = req.rpc_params.owner; + account.iam_arn = req.rpc_params.iam_arn; + account.iam_path = req.rpc_params.iam_path; + } + + await system_store.make_changes({ + insert: { + accounts: [account], + roles, + master_keys: [account_access_info.account_mkey] + } + }); + + const created_account = system_store.data.get_by_id(account._id); + const auth = { + account_id: created_account._id + }; + // since we created the first system for this account + // we expect just one system, but use _.each to get it from the map + const current_system = (req.system && req.system._id) || sys_id; + _.each(created_account.roles_by_system, (sys_roles, system_id) => { + //we cannot assume only one system. + if (current_system.toString() === system_id) { + auth.system_id = system_id; + auth.role = sys_roles[0]; + } + }); + return { + token: auth_server.make_auth_token(auth), + access_keys: account_access_info.decrypted_access_keys, + id: req.rpc_params.is_iam ? created_account._id.toString() : undefined, + create_date: req.rpc_params.is_iam ? new Date() : undefined, + }; +} + +function delete_account(req, account_to_delete) { + const roles_to_delete = system_store.data.roles + .filter( + role => String(role.account._id) === String(account_to_delete._id) + ) + .map( + role => role._id + ); + //const email = account_to_delete.email instanceof SensitiveString ? account_to_delete.email.unwrap() : username; + + return system_store.make_changes({ + remove: { + accounts: [account_to_delete._id], + roles: roles_to_delete + } + }) + .then( + val => { + Dispatcher.instance().activity({ + event: 'account.delete', + level: 'info', + system: req.system && req.system._id, + actor: req.account && req.account._id, + account: account_to_delete._id, + desc: `${account_to_delete.email.unwrap()} was deleted by ${req.account && req.account.email.unwrap()}`, + }); + return val; + }, + err => { + Dispatcher.instance().activity({ + event: 'account.delete', + level: 'alert', + system: req.system && req.system._id, + actor: req.account && req.account._id, + account: account_to_delete._id, + desc: `Error: ${account_to_delete.email.unwrap()} failed to delete by ${req.account && req.account.email.unwrap()}`, + }); + throw err; + } + ); +} + +/** + * + * GENERATE_ACCOUNT_KEYS + * + */ +async function generate_account_keys(req) { + const account = system_store.get_account_by_email(req.rpc_params.email); + if (!account) { + throw new RpcError('NO_SUCH_ACCOUNT', 'No such account email: ' + req.rpc_params.email); + } + + if (!req.system) { + req.system = system_store.data.systems[0]; + } + + if (req.system && req.account) { + if (!is_support_or_admin_or_me(req.system, req.account, account)) { + throw new RpcError('UNAUTHORIZED', 'Cannot update account'); + } + } + if (account.is_support) { + throw new RpcError('FORBIDDEN', 'Cannot update support account'); + } + const access_keys = cloud_utils.generate_access_keys(); + const decrypted_access_keys = _.cloneDeep(access_keys); + access_keys.secret_key = system_store.master_key_manager.encrypt_sensitive_string_with_master_key_id( + access_keys.secret_key, account.master_key_id._id); + + await system_store.make_changes({ + update: { + accounts: [{ + _id: account._id, + access_keys: [ + access_keys + ] + }] + } + }); + + Dispatcher.instance().activity({ + event: 'account.generate_credentials', + level: 'info', + system: req.system && req.system._id, + actor: req.account && req.account._id, + account: account._id, + desc: `Credentials for ${account.email.unwrap()} were regenerated ${req.account && 'by ' + req.account.email.unwrap()}`, + }); + if (req.rpc_params.is_iam) { + return decrypted_access_keys; + } +} + +function is_support_or_admin_or_me(system, account, target_account) { + return account.is_support || + (target_account && String(target_account._id) === String(account._id)) || + ( + system && account.roles_by_system[system._id].some( + role => role === 'admin' || role === 'operator' + ) + ); +} + +function create_access_key_auth(req, account, is_iam) { + + const account_mkey = system_store.master_key_manager.new_master_key({ + description: `master key of ${account._id} account`, + cipher_type: system_store.data.systems[0].master_key_id.cipher_type, + master_key_id: system_store.data.systems[0].master_key_id._id + }); + account.master_key_id = account_mkey._id; + let decrypted_access_keys; + if (!is_iam) { + if (account.name.unwrap() === 'demo' && account.email.unwrap() === 'demo@noobaa.com') { + account.access_keys = [demo_access_keys]; + } else { + const access_keys = req.rpc_params.access_keys || [cloud_utils.generate_access_keys()]; + if (!access_keys.length) throw new RpcError('FORBIDDEN', 'cannot create account without access_keys'); + account.access_keys = access_keys; + } + decrypted_access_keys = _.cloneDeep(account.access_keys); + account.access_keys[0] = { + access_key: account.access_keys[0].access_key, + secret_key: system_store.master_key_manager.encrypt_sensitive_string_with_master_key_id( + account.access_keys[0].secret_key, account_mkey._id) + }; + } + + return { + decrypted_access_keys, + account_mkey + }; +} + +function validate_assume_role_policy(policy) { + const all_op_names = Object.values(OP_NAME_TO_ACTION); + for (const statement of policy.statement) { + for (const principal of statement.principal) { + if (principal.unwrap() !== '*') { + const account = system_store.get_account_by_email(principal); + if (!account) { + throw new RpcError('MALFORMED_POLICY', 'Invalid principal in policy', { detail: principal }); + } + } + } + for (const action of statement.action) { + if (action !== 'sts:*' && !all_op_names.includes(action)) { + throw new RpcError('MALFORMED_POLICY', 'Policy has invalid action', { detail: action }); + } + } + } +} + +// User name is first part is user provided name, and second part +// is root account name, This will make the user name uniq accross system. +function populate_username(username, requesting_account_name) { + return new SensitiveString(`${username}:${requesting_account_name}`); +} + +function get_iam_username(requested_account_name) { + return requested_account_name.split(IAM_SPLIT_CHARACTERS)[0]; +} + +function _check_if_account_exists(action, email) { + const account = system_store.get_account_by_email(email); + email = email instanceof SensitiveString ? email.unwrap() : email; + if (!account) { + dbg.error(`AccountSpaceNB.${action} username does not exist`, email); + const message_with_details = `The user with name ${email} cannot be found.`; + const { code, http_code, type } = IamError.NoSuchEntity; + throw new IamError({ code, message: message_with_details, http_code, type }); + } +} + +function _check_root_account_owns_user(root_account, user_account) { + if (user_account.owner === undefined) return false; + let root_account_id; + let owner_account_id; + if (typeof root_account._id === 'object') { + root_account_id = String(root_account._id); + } else { + root_account_id = root_account._id; + } + + if (typeof user_account.owner === 'object') { + owner_account_id = String(user_account.owner._id); + } else { + owner_account_id = user_account.owner._id; + } + return root_account_id === owner_account_id; +} + +function _check_if_requesting_account_is_root_account(action, requesting_account, user_details = {}) { + const is_root_account = _check_root_account(requesting_account); + dbg.log1(`AccountSpaceNB.${action} requesting_account ID: ${requesting_account._id}` + + `name: ${requesting_account.name.unwrap()}`, 'is_root_account', is_root_account); + if (!is_root_account) { + dbg.error(`AccountSpaceNB.${action} requesting account is not a root account`, + requesting_account); + _throw_access_denied_error(action, requesting_account, user_details, "USER"); + } +} + +function _check_username_already_exists(action, params, requesting_account) { + const username = populate_username(params.username, requesting_account.name.unwrap()); + const account = system_store.get_account_by_email(username); + if (account) { + dbg.error(`AccountSpaceNB.${action} username already exists`, params.username); + const message_with_details = `User with name ${params.username} already exists.`; + const { code, http_code, type } = IamError.EntityAlreadyExists; + throw new IamError({ code, message: message_with_details, http_code, type }); + } +} + +function _check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account) { + const is_requested_account_root_account = _check_root_account(requested_account); + dbg.log1(`AccountSpaceNB.${action} requested_account ID: ${requested_account._id} name: ${requested_account.name}`, + 'is_requested_account_root_account', is_requested_account_root_account); + // access to root account is allowed to root account that has iam_operate_on_root_account true + if (is_requested_account_root_account && !requesting_account.iam_operate_on_root_account) { + _throw_error_perform_action_on_another_root_account(action, + requesting_account, requested_account); + } + // access to IAM user is allowed to root account that either iam_operate_on_root_account undefined or false + if (requesting_account.iam_operate_on_root_account && !is_requested_account_root_account) { + _throw_error_perform_action_from_root_accounts_manager_on_iam_user(action, + requesting_account, requested_account); + } +} + +function _check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account) { + if (requesting_account.iam_operate_on_root_account) return; + const is_user_account_to_get_owned_by_root_user = _check_root_account_owns_user(requesting_account, requested_account); + if (!is_user_account_to_get_owned_by_root_user) { + const username = requested_account.name instanceof SensitiveString ? + requested_account.name.unwrap() : requested_account.name; + dbg.error(`AccountSpaceNB.${action} requested account is not owned by root account`, username); + 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 }); + } +} + +/** +* _returned_username would return the username of IAM Access key API: + * 1. undefined - for root accounts manager on root account (no username, only account name) + * for root account on itself + * 2. username - for IAM user + * @param {object} requesting_account + * @param {Object} username + * @param {boolean} on_itself + */ +function _returned_username(requesting_account, username, on_itself) { + if ((requesting_account.iam_operate_on_root_account) || + (_check_root_account(requesting_account) && on_itself)) { + return undefined; + } + return username instanceof SensitiveString ? username.unwrap() : username; +} + +function _throw_error_perform_action_from_root_accounts_manager_on_iam_user(action, requesting_account, requested_account) { + dbg.error(`AccountSpaceNB.${action} root accounts manager cannot perform actions on IAM users`, + requesting_account, requested_account); + throw new IamError(IamError.NotAuthorized); +} + +// TODO: move to IamError class with a template +function _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(`AccountSpaceNB.${action} root account of requested account is different than requesting root account`, + requesting_account._id.toString(), username); + 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 }); +} + +function _throw_access_denied_error(action, requesting_account, details, entity) { + const full_action_name = get_action_message_title(action); + const account_id_for_arn = _get_account_owner_id_for_arn(requesting_account); + const arn_for_requesting_account = create_arn_for_user(account_id_for_arn, + requesting_account.name.unwrap(), requesting_account.path || IAM_DEFAULT_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 === 'USER') { + let user_message; + if (action === IAM_ACTIONS.LIST_ACCESS_KEYS) { + user_message = `user ${details.username}`; + } else { + user_message = create_arn_for_user(account_id_for_arn, details.username, details.path); + } + message_with_details = basic_message + + `${user_message} because no identity-based policy allows the ${full_action_name} action`; + } else { + 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 }); +} + +/** + * _get_account_owner_id_for_arn will return the account ID + * that we need for creating the ARN, the cases: + * 1. iam user - it's owner property + * 2. root account - it's account ID + * 3. root accounts manager - the account ID of the account that it operates on + * @param {object} requesting_account + * @param {object} [requested_account] + * @returns {string|undefined} + */ +function _get_account_owner_id_for_arn(requesting_account, requested_account) { + if (!requesting_account.iam_operate_on_root_account) { + if (requesting_account.owner !== undefined) { + return requesting_account.owner; + } + return requesting_account._id; + } + return requested_account?._id; +} + +function _check_root_account(account) { + return account.owner === undefined; +} + + + +function validate_create_account_permissions(req) { + const account = req.account; + //For new system creation, nothing to be checked + if (req.rpc_params.new_system_parameters) return; + + //Only allow support, admin/operator roles and UI login enabled accounts to create new accounts + if (!account.is_support && + !account.has_login && + !(account.roles_by_system[req.system._id].some( + role => role === 'admin' || role === 'operator' + ))) { + throw new RpcError('UNAUTHORIZED', 'Cannot create new account'); + } +} + +function validate_create_account_params(req) { + // find none-internal pools + const has_non_internal_resources = (req.system && req.system.pools_by_name) ? + Object.values(req.system.pools_by_name).some(p => !p.is_default_pool) : + false; + + if (req.rpc_params.name.unwrap() !== req.rpc_params.name.unwrap().trim()) { + throw new RpcError('BAD_REQUEST', 'system name must not contain leading or trailing spaces'); + } + + if (system_store.get_account_by_email(req.rpc_params.email)) { + throw new RpcError('BAD_REQUEST', 'email address already registered'); + } + + if (req.rpc_params.s3_access) { + if (!req.rpc_params.new_system_parameters) { + if (req.system.pools_by_name === 0) { + throw new RpcError('No resources in the system - Can\'t create accounts'); + } + + if (req.rpc_params.allow_bucket_creation && !req.rpc_params.default_resource) { //default resource needed only if new bucket can be created + if (has_non_internal_resources) { // has resources which is not internal - must supply resource + throw new RpcError('BAD_REQUEST', 'Enabling S3 requires providing default_resource'); + } + } + } + + if (req.rpc_params.new_system_parameters) { + if (!req.rpc_params.new_system_parameters.default_resource) { + throw new RpcError( + 'BAD_REQUEST', + 'Creating new system with enabled S3 access for owner requires providing default_resource' + ); + } + } + } + + if (req.rpc_params.has_login) { + if (!req.rpc_params.password) { + throw new RpcError('BAD_REQUEST', 'Password is missing'); + } + + // Verify that account with login access have full s3 access permissions. + const { default_resource } = req.rpc_params.new_system_parameters || req.rpc_params; + const allow_bucket_creation = req.rpc_params.new_system_parameters ? + true : + req.rpc_params.allow_bucket_creation; + + if ( + !req.rpc_params.s3_access || + (has_non_internal_resources && !default_resource) || + !allow_bucket_creation + ) { + throw new RpcError('BAD_REQUEST', 'Accounts with login access must have full s3 access permissions'); + } + + } else if (req.rpc_params.password) { + throw new RpcError('BAD_REQUEST', 'Password should not be sent'); + } +} + + +exports.delete_account = delete_account; +exports.create_account = create_account; +exports.generate_account_keys = generate_account_keys; +exports.populate_username = populate_username; +exports.get_iam_username = get_iam_username; +exports._check_if_requesting_account_is_root_account = _check_if_requesting_account_is_root_account; +exports._check_username_already_exists = _check_username_already_exists; +exports.validate_create_account_permissions = validate_create_account_permissions; +exports.validate_create_account_params = validate_create_account_params; +exports._check_if_account_exists = _check_if_account_exists; +exports._returned_username = _returned_username; +exports._check_if_requested_is_owned_by_root_account = _check_if_requested_is_owned_by_root_account; +exports._check_if_requested_account_is_root_account_or_IAM_user = _check_if_requested_account_is_root_account_or_IAM_user;