Skip to content

Commit 722f219

Browse files
committed
IAM | Bucket policy Principal validation
Signed-off-by: Naveen Paul <napaul@redhat.com>
1 parent 477d3ae commit 722f219

File tree

13 files changed

+182
-82
lines changed

13 files changed

+182
-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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const _ = require('lodash');
55
const dbg = require('../../util/debug_module')(__filename);
66
const s3_utils = require('./s3_utils');
77
const RpcError = require('../../rpc/rpc_error');
8+
const iam_utils = require('../../endpoint/iam/iam_utils');
89

910
const OP_NAME_TO_ACTION = Object.freeze({
1011
delete_bucket_analytics: { regular: "s3:PutAnalyticsConfiguration" },
@@ -341,7 +342,14 @@ function allows_public_access(policy) {
341342
return false;
342343
}
343344

345+
function get_bucket_policy_principal_arn(account) {
346+
const bucket_policy_arn = account.owner ? iam_utils.create_arn_for_user(account.owner, account.name.unwrap().split(':')[0], account.iam_path) :
347+
iam_utils.create_arn_for_root(account._id);
348+
return bucket_policy_arn;
349+
}
350+
344351
exports.OP_NAME_TO_ACTION = OP_NAME_TO_ACTION;
345352
exports.has_bucket_policy_permission = has_bucket_policy_permission;
346353
exports.validate_s3_policy = validate_s3_policy;
347354
exports.allows_public_access = allows_public_access;
355+
exports.get_bucket_policy_principal_arn = get_bucket_policy_principal_arn;

src/endpoint/s3/s3_rest.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ async function authorize_request(req) {
231231
async function authorize_request_policy(req) {
232232
if (!req.params.bucket) return;
233233
if (req.op_name === 'put_bucket') return;
234-
235234
// owner_account is { id: bucket.owner_account, email: bucket.bucket_owner };
236235
const {
237236
s3_policy,
@@ -254,6 +253,7 @@ async function authorize_request_policy(req) {
254253
const account = req.object_sdk.requesting_account;
255254
const account_identifier_name = req.object_sdk.nsfs_config_root ? account.name.unwrap() : account.email.unwrap();
256255
const account_identifier_id = req.object_sdk.nsfs_config_root ? account._id : undefined;
256+
const account_identifier_arn = s3_bucket_policy_utils.get_bucket_policy_principal_arn(account);
257257

258258
// deny delete_bucket permissions from bucket_claim_owner accounts (accounts that were created by OBC from openshift\k8s)
259259
// the OBC bucket can still be delete by normal accounts according to the access policy which is checked below
@@ -292,6 +292,7 @@ async function authorize_request_policy(req) {
292292
// in case we have bucket policy
293293
let permission_by_id;
294294
let permission_by_name;
295+
let permission_by_arn;
295296

296297
// In NC, we allow principal to be:
297298
// 1. account name (for backwards compatibility)
@@ -303,15 +304,25 @@ async function authorize_request_policy(req) {
303304
dbg.log3('authorize_request_policy: permission_by_id', permission_by_id);
304305
}
305306
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);
306-
307307
if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
308308
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
309309
s3_policy, account_identifier_name, method, arn_path, req, public_access_block?.restrict_public_buckets
310310
);
311311
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
312312
}
313313
if (permission_by_name === "DENY") throw new S3Error(S3Error.AccessDenied);
314-
if ((permission_by_id === "ALLOW" || permission_by_name === "ALLOW") || is_owner) return;
314+
// Containerized deployment always will have account_identifier_id undefined
315+
// Policy permission is validated by account arn
316+
if (!account_identifier_id) {
317+
permission_by_arn = await s3_bucket_policy_utils.has_bucket_policy_permission(
318+
s3_policy, account_identifier_arn, method, arn_path, req, public_access_block?.restrict_public_buckets
319+
);
320+
dbg.log3('authorize_request_policy: permission_by_arn', permission_by_arn);
321+
}
322+
dbg.log3('authorize_request_policy: permission_by_arn', permission_by_arn);
323+
if (permission_by_arn === "DENY") throw new S3Error(S3Error.AccessDenied);
324+
325+
if ((permission_by_id === "ALLOW" || permission_by_name === "ALLOW" || permission_by_arn === "ALLOW") || is_owner) return;
315326

316327
throw new S3Error(S3Error.AccessDenied);
317328
}

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: 30 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,39 @@ async function get_bucket_policy(req) {
512514
};
513515
}
514516

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

516545
async function put_bucket_policy(req) {
517546
dbg.log0('put_bucket_policy:', req.rpc_params);
518547
const bucket = find_bucket(req, req.rpc_params.name);
519548
await bucket_policy_utils.validate_s3_policy(req.rpc_params.policy, bucket.name,
520-
principal => system_store.get_account_by_email(principal));
549+
principal => principal_validation_handler(principal));
521550

522551
if (
523552
bucket.public_access_block?.block_public_policy &&

0 commit comments

Comments
 (0)