Skip to content

Commit 221b447

Browse files
committed
IAM | Bucket policy Principal validation
Signed-off-by: Naveen Paul <napaul@redhat.com>
1 parent 45fe1e7 commit 221b447

File tree

13 files changed

+223
-82
lines changed

13 files changed

+223
-82
lines changed

src/api/account_api.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ module.exports = {
620620
type: 'object',
621621
required: ['name', 'email', 'has_s3_access'],
622622
properties: {
623+
_id: { type: 'string'},
623624
name: { $ref: 'common_api#/definitions/account_name' },
624625
email: { $ref: 'common_api#/definitions/email' },
625626
is_support: {
@@ -661,6 +662,12 @@ module.exports = {
661662
bucket_claim_owner: {
662663
$ref: 'common_api#/definitions/bucket_name',
663664
},
665+
owner: {
666+
type: 'string',
667+
},
668+
iam_path: {
669+
type: 'string',
670+
},
664671
external_connections: {
665672
type: 'object',
666673
properties: {

src/endpoint/iam/iam_utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ function check_iam_path_was_set(iam_path) {
6262
return iam_path && iam_path !== iam_constants.IAM_DEFAULT_PATH;
6363
}
6464

65+
function get_iam_username(requested_account_name) {
66+
return requested_account_name.split(iam_constants.IAM_SPLIT_CHARACTERS)[0];
67+
}
68+
6569
/**
6670
* parse_max_items converts the input to the needed type
6771
* assuming that we've got only sting type
@@ -818,6 +822,7 @@ exports.create_arn_for_user = create_arn_for_user;
818822
exports.create_arn_for_root = create_arn_for_root;
819823
exports.get_action_message_title = get_action_message_title;
820824
exports.check_iam_path_was_set = check_iam_path_was_set;
825+
exports.get_iam_username = get_iam_username;
821826
exports.parse_max_items = parse_max_items;
822827
exports.validate_params = validate_params;
823828
exports.validate_iam_path = validate_iam_path;

src/endpoint/s3/s3_bucket_policy_utils.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
9494

9595
const qm_regex = /\?/g;
9696
const ar_regex = /\*/g;
97+
const IAM_DEFAULT_PATH = '/';
9798

9899
const predicate_map = {
99100
'StringEquals': (request_value, policy_value) => request_value === policy_value,
@@ -353,7 +354,55 @@ function allows_public_access(policy) {
353354
return false;
354355
}
355356

357+
/**
358+
* Return IAM ARN for account, if account dont have owner
359+
* and User, if the user do have account ower
360+
*
361+
* @param {Object} account
362+
* @returns {string}
363+
*/
364+
function get_bucket_policy_principal_arn(account) {
365+
const bucket_policy_arn = account.owner ? create_arn_for_user(account.owner, account.name.unwrap().split(':')[0], account.iam_path) :
366+
create_arn_for_root(account._id);
367+
return bucket_policy_arn;
368+
}
369+
370+
/**
371+
* create_arn_for_root creates the AWS ARN for root account user
372+
* see: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns
373+
* @param {nb.ID} account_id (the root user account id)
374+
*/
375+
function create_arn_for_root(account_id) {
376+
return `arn:aws:iam::${account_id}:root`;
377+
}
378+
379+
/**
380+
* create_arn_for_user creates the AWS ARN for user
381+
* see: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns
382+
* @param {nb.ID} account_id (the root user account id)
383+
* @param {string} username
384+
* @param {string} iam_path
385+
*/
386+
function create_arn_for_user(account_id, username, iam_path) {
387+
const basic_structure = `arn:aws:iam::${account_id}:user`;
388+
if (username === undefined) return `${basic_structure}/`;
389+
if (check_iam_path_was_set(iam_path)) {
390+
return `${basic_structure}${iam_path}${username}`;
391+
}
392+
return `${basic_structure}/${username}`;
393+
}
394+
395+
/**
396+
* check_iam_path_was_set return true if the iam_path was set
397+
* @param {string} iam_path
398+
*/
399+
function check_iam_path_was_set(iam_path) {
400+
return iam_path && iam_path !== IAM_DEFAULT_PATH;
401+
}
402+
356403
exports.OP_NAME_TO_ACTION = OP_NAME_TO_ACTION;
357404
exports.has_bucket_policy_permission = has_bucket_policy_permission;
358405
exports.validate_s3_policy = validate_s3_policy;
359406
exports.allows_public_access = allows_public_access;
407+
exports.get_bucket_policy_principal_arn = get_bucket_policy_principal_arn;
408+
exports.create_arn_for_root = create_arn_for_root;

src/endpoint/s3/s3_rest.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ async function authorize_request(req) {
233233
async function authorize_request_policy(req) {
234234
if (!req.params.bucket) return;
235235
if (req.op_name === 'put_bucket') return;
236-
237236
// owner_account is { id: bucket.owner_account, email: bucket.bucket_owner };
238237
const {
239238
s3_policy,
@@ -256,6 +255,7 @@ async function authorize_request_policy(req) {
256255
const account = req.object_sdk.requesting_account;
257256
const account_identifier_name = req.object_sdk.nsfs_config_root ? account.name.unwrap() : account.email.unwrap();
258257
const account_identifier_id = req.object_sdk.nsfs_config_root ? account._id : undefined;
258+
const account_identifier_arn = s3_bucket_policy_utils.get_bucket_policy_principal_arn(account);
259259

260260
// deny delete_bucket permissions from bucket_claim_owner accounts (accounts that were created by OBC from openshift\k8s)
261261
// the OBC bucket can still be delete by normal accounts according to the access policy which is checked below
@@ -294,6 +294,7 @@ async function authorize_request_policy(req) {
294294
// in case we have bucket policy
295295
let permission_by_id;
296296
let permission_by_name;
297+
let permission_by_arn;
297298

298299
// In NC, we allow principal to be:
299300
// 1. account name (for backwards compatibility)
@@ -307,7 +308,6 @@ async function authorize_request_policy(req) {
307308
dbg.log3('authorize_request_policy: permission_by_id', permission_by_id);
308309
}
309310
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);
310-
311311
if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
312312
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
313313
s3_policy, account_identifier_name, method, arn_path, req,
@@ -316,7 +316,17 @@ async function authorize_request_policy(req) {
316316
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
317317
}
318318
if (permission_by_name === "DENY") throw new S3Error(S3Error.AccessDenied);
319-
if ((permission_by_id === "ALLOW" || permission_by_name === "ALLOW") || is_owner) return;
319+
// Containerized deployment always will have account_identifier_id undefined
320+
// Policy permission is validated by account arn
321+
if (!account_identifier_id) {
322+
permission_by_arn = await s3_bucket_policy_utils.has_bucket_policy_permission(
323+
s3_policy, account_identifier_arn, method, arn_path, req, public_access_block?.restrict_public_buckets
324+
);
325+
dbg.log3('authorize_request_policy: permission_by_arn', permission_by_arn);
326+
}
327+
if (permission_by_arn === "DENY") throw new S3Error(S3Error.AccessDenied);
328+
329+
if ((permission_by_id === "ALLOW" || permission_by_name === "ALLOW" || permission_by_arn === "ALLOW") || is_owner) return;
320330

321331
throw new S3Error(S3Error.AccessDenied);
322332
}

src/sdk/accountspace_nb.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class AccountSpaceNB {
8585
const action = IAM_ACTIONS.GET_USER;
8686
const requesting_account = system_store.get_account_by_email(account_sdk.requesting_account.email);
8787
const requested_account = validate_and_return_requested_account(params, action, requesting_account);
88-
const username = account_util.get_iam_username(params.username || requested_account.name.unwrap());
88+
const username = iam_utils.get_iam_username(params.username || requested_account.name.unwrap());
8989
const iam_arn = iam_utils.create_arn_for_user(requesting_account._id.toString(), username,
9090
requested_account.iam_path || IAM_DEFAULT_PATH);
9191
const reply = {
@@ -110,7 +110,7 @@ class AccountSpaceNB {
110110
account_util._check_if_account_exists(action, old_username);
111111
const requested_account = system_store.get_account_by_email(old_username);
112112
let iam_path = requested_account.iam_path;
113-
let user_name = account_util.get_iam_username(requested_account.name.unwrap());
113+
let user_name = iam_utils.get_iam_username(requested_account.name.unwrap());
114114
account_util._check_username_already_exists(action, { username: params.new_username }, requesting_account);
115115
account_util._check_if_requested_account_is_root_account_or_IAM_user(action, requesting_account, requested_account);
116116
account_util._check_if_requested_is_owned_by_root_account(action, requesting_account, requested_account);
@@ -173,7 +173,7 @@ class AccountSpaceNB {
173173
return account.owner?._id.toString() === requesting_account._id.toString();
174174
});
175175
let members = _.map(requesting_account_iam_users, function(iam_user) {
176-
const iam_username = account_util.get_iam_username(iam_user.name.unwrap());
176+
const iam_username = iam_utils.get_iam_username(iam_user.name.unwrap());
177177
const iam_path = iam_user.iam_path || IAM_DEFAULT_PATH;
178178
const member = {
179179
user_id: iam_user._id.toString(),
@@ -218,7 +218,7 @@ class AccountSpaceNB {
218218
}
219219

220220
return {
221-
username: account_util.get_iam_username(requested_account.name.unwrap()),
221+
username: iam_utils.get_iam_username(requested_account.name.unwrap()),
222222
access_key: iam_access_key.access_key.unwrap(),
223223
create_date: iam_access_key.creation_date,
224224
status: ACCESS_KEY_STATUS_ENUM.ACTIVE,
@@ -238,7 +238,7 @@ class AccountSpaceNB {
238238
region: dummy_region, // GAP
239239
last_used_date: Date.now(), // GAP
240240
service_name: dummy_service_name, // GAP
241-
username: username ? account_util.get_iam_username(username) : undefined,
241+
username: username ? iam_utils.get_iam_username(username) : undefined,
242242
};
243243
}
244244

src/server/common_services/auth_server.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const addr_utils = require('../../util/addr_utils');
1515
const kube_utils = require('../../util/kube_utils');
1616
const jwt_utils = require('../../util/jwt_utils');
1717
const config = require('../../../config');
18+
const iam_utils = require('../../endpoint/iam/iam_utils');
1819
const s3_bucket_policy_utils = require('../../endpoint/s3/s3_bucket_policy_utils');
1920

2021

@@ -548,10 +549,11 @@ async function has_bucket_action_permission(bucket, account, action, req_query,
548549
if (!action) {
549550
throw new Error('has_bucket_action_permission: action is required');
550551
}
551-
552+
const arn = account.owner ? iam_utils.create_arn_for_user(account.owner._id.toString(), account.name.unwrap().split(':')[0], account.iam_path) :
553+
iam_utils.create_arn_for_root(account._id);
552554
const result = await s3_bucket_policy_utils.has_bucket_policy_permission(
553555
bucket_policy,
554-
account.email.unwrap(),
556+
arn,
555557
action,
556558
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
557559
req_query

src/server/system_services/account_server.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ function get_account_info(account, include_connection_cache) {
988988
'has_login',
989989
'allowed_ips',
990990
);
991-
991+
info._id = account._id.toString();
992992
if (account.is_support) {
993993
info.is_support = true;
994994
info.has_login = true;
@@ -998,7 +998,10 @@ function get_account_info(account, include_connection_cache) {
998998
secret_key: 'Not Accesible'
999999
}];
10001000
}
1001-
1001+
if (account.owner) {
1002+
info.owner = account.owner._id.toString();
1003+
}
1004+
info.iam_path = account.iam_path;
10021005
if (account.next_password_change) {
10031006
info.next_password_change = account.next_password_change.getTime();
10041007
}

src/server/system_services/bucket_server.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const GoogleStorage = require('../../util/google_storage_wrap');
1111

1212
const P = require('../../util/promise');
1313
const dbg = require('../../util/debug_module')(__filename);
14+
const SensitiveString = require('../../util/sensitive_string');
1415
const config = require('../../../config');
1516
const MDStore = require('../object_services/md_store').MDStore;
1617
const BucketStatsStore = require('../analytic_services/bucket_stats_store').BucketStatsStore;
@@ -39,6 +40,7 @@ const bucket_semaphore = new KeysSemaphore(1);
3940
const Quota = require('../system_services/objects/quota');
4041
const { STORAGE_CLASS_GLACIER_IR } = require('../../endpoint/s3/s3_utils');
4142
const noobaa_s3_client = require('../../sdk/noobaa_s3_client/noobaa_s3_client');
43+
const string_utils = require('../../util/string_utils');
4244

4345
const VALID_BUCKET_NAME_REGEXP =
4446
/^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/;
@@ -512,12 +514,40 @@ async function get_bucket_policy(req) {
512514
};
513515
}
514516

517+
/**
518+
Validate and return account by principal ARN.
519+
1. validate basic ARN, like arn prefix `arn:aws:iam::`
520+
2. If principal ARN ends with `root` suffix, it's an account and get account with id
521+
eg: arn:aws:iam::${account_id}:root
522+
3. if principal ARN contains `user`, it's an IAM user and get account with username and id
523+
eg: arn:aws:iam::${account_id}:user/${iam_path}/${user_name}
524+
account email = ${iam_user_name}:${account_id}
525+
526+
@param {SensitiveString | String} principal Bucket policy principal
527+
*/
528+
async function get_account_by_principal(principal) {
529+
const principal_as_string = principal instanceof SensitiveString ? principal.unwrap() : principal;
530+
const root_sufix = 'root';
531+
const user_sufix = 'user';
532+
const arn_parts = principal_as_string.split(':');
533+
if (!string_utils.AWS_IAM_ARN_REGEXP.test(principal_as_string)) {
534+
return;
535+
}
536+
const account_id = arn_parts[4];
537+
if (principal_as_string.endsWith(root_sufix) && !arn_parts[5].startsWith(user_sufix)) {
538+
return system_store.data.accounts.find(account => account._id.toString() === account_id);
539+
} else if (arn_parts[5] && arn_parts[5].startsWith(user_sufix)) {
540+
const arn_path_parts = principal_as_string.split('/');
541+
const iam_user_name = arn_path_parts[arn_path_parts.length - 1].trim();
542+
return system_store.get_account_by_email(new SensitiveString(`${iam_user_name}:${account_id}`));
543+
}
544+
}
515545

516546
async function put_bucket_policy(req) {
517547
dbg.log0('put_bucket_policy:', req.rpc_params);
518548
const bucket = find_bucket(req, req.rpc_params.name);
519549
await bucket_policy_utils.validate_s3_policy(req.rpc_params.policy, bucket.name,
520-
principal => system_store.get_account_by_email(principal));
550+
principal => get_account_by_principal(principal));
521551

522552
if (
523553
bucket.public_access_block?.block_public_policy &&

0 commit comments

Comments
 (0)