Skip to content

Commit 19dbd17

Browse files
committed
IAM | Principal validation and S3 permission updated with ID
Signed-off-by: Naveen Paul <napaul@redhat.com>
1 parent 33924d7 commit 19dbd17

File tree

7 files changed

+289
-58
lines changed

7 files changed

+289
-58
lines changed

src/endpoint/s3/s3_bucket_policy_utils.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,26 @@ async function _is_object_version_fit(req, predicate, value) {
150150
return res;
151151
}
152152

153+
/**
154+
* has_bucket_policy_permission validate the bucket policy principal
155+
*
156+
* @param {object} policy
157+
* @param {string[] | string} account
158+
* @param {string[] | string} method
159+
* @param {string} arn_path
160+
* @param {object} req
161+
*/
153162
async function has_bucket_policy_permission(policy, account, method, arn_path, req,
154163
{ disallow_public_access = false, should_pass_principal = true } = {}) {
155164
const [allow_statements, deny_statements] = _.partition(policy.Statement, statement => statement.Effect === 'Allow');
156165

157166
// the case where the permission is an array started in op get_object_attributes
158167
const method_arr = Array.isArray(method) ? method : [method];
168+
const account_arr = Array.isArray(account) ? account : [account];
159169

160170
// look for explicit denies
161171
const res_arr_deny = await is_statement_fit_of_method_array(
162-
deny_statements, account, method_arr, arn_path, req, {
172+
deny_statements, account_arr, method_arr, arn_path, req, {
163173
disallow_public_access: false, // No need to disallow in "DENY"
164174
should_pass_principal
165175
}
@@ -168,7 +178,7 @@ async function has_bucket_policy_permission(policy, account, method, arn_path, r
168178

169179
// look for explicit allows
170180
const res_arr_allow = await is_statement_fit_of_method_array(
171-
allow_statements, account, method_arr, arn_path, req, {
181+
allow_statements, account_arr, method_arr, arn_path, req, {
172182
disallow_public_access,
173183
should_pass_principal
174184
});
@@ -191,14 +201,14 @@ function _is_action_fit(method, statement) {
191201
return statement.Action ? action_fit : !action_fit;
192202
}
193203

194-
function _is_principal_fit(account, statement, ignore_public_principal = false) {
204+
function _is_principal_fit(account_arr, statement, ignore_public_principal = false) {
195205
let statement_principal = statement.Principal || statement.NotPrincipal;
196206

197207
let principal_fit = false;
198208
statement_principal = statement_principal.AWS ? statement_principal.AWS : statement_principal;
199209
for (const principal of _.flatten([statement_principal])) {
200-
dbg.log1('bucket_policy: ', statement.Principal ? 'Principal' : 'NotPrincipal', ' fit?', principal, account);
201-
if ((principal.unwrap() === '*') || (principal.unwrap() === account)) {
210+
dbg.log1('bucket_policy: ', statement.Principal ? 'Principal' : 'NotPrincipal', ' fit?', principal, account_arr);
211+
if ((principal.unwrap() === '*') || account_arr.includes(principal.unwrap())) {
202212
if (ignore_public_principal && principal.unwrap() === '*' && statement.Principal) {
203213
// Ignore the "fit" if ignore_public_principal is requested
204214
continue;
@@ -226,19 +236,19 @@ function _is_resource_fit(arn_path, statement) {
226236
return statement.Resource ? resource_fit : !resource_fit;
227237
}
228238

229-
async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req,
239+
async function is_statement_fit_of_method_array(statements, account_arr, method_arr, arn_path, req,
230240
{ disallow_public_access = false, should_pass_principal = true } = {}) {
231241
return Promise.all(method_arr.map(method_permission =>
232-
_is_statements_fit(statements, account, method_permission, arn_path, req, { disallow_public_access, should_pass_principal })));
242+
_is_statements_fit(statements, account_arr, method_permission, arn_path, req, { disallow_public_access, should_pass_principal })));
233243
}
234244

235-
async function _is_statements_fit(statements, account, method, arn_path, req,
245+
async function _is_statements_fit(statements, account_arr, method, arn_path, req,
236246
{ disallow_public_access = false, should_pass_principal = true} = {}) {
237247
for (const statement of statements) {
238248
const action_fit = _is_action_fit(method, statement);
239249
// When evaluating IAM user inline policies, should_pass_principal is false since these policies
240250
// don't have a Principal field (the principal is implicitly the user)
241-
const principal_fit = should_pass_principal ? _is_principal_fit(account, statement, disallow_public_access) : true;
251+
const principal_fit = should_pass_principal ? _is_principal_fit(account_arr, statement, disallow_public_access) : true;
242252
const resource_fit = _is_resource_fit(arn_path, statement);
243253
const condition_fit = await _is_condition_fit(statement, req, method);
244254

@@ -363,7 +373,7 @@ function allows_public_access(policy) {
363373
*/
364374
function get_bucket_policy_principal_arn(account) {
365375
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);
376+
create_arn_for_root(account._id.toString());
367377
return bucket_policy_arn;
368378
}
369379

src/endpoint/s3/s3_rest.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const http_utils = require('../../util/http_utils');
1414
const signature_utils = require('../../util/signature_utils');
1515
const config = require('../../../config');
1616
const s3_utils = require('./s3_utils');
17-
const { _create_detailed_message_for_iam_user_access_in_s3 } = require('../iam/iam_utils'); // for IAM policy
17+
const { _create_detailed_message_for_iam_user_access_in_s3, get_owner_account_id } = require('../iam/iam_utils'); // for IAM policy
1818

1919
const S3_MAX_BODY_LEN = 4 * 1024 * 1024;
2020

@@ -252,7 +252,8 @@ async function authorize_request_policy(req) {
252252
const account = req.object_sdk.requesting_account;
253253
const is_nc_deployment = Boolean(req.object_sdk.nsfs_config_root);
254254
const account_identifier_name = is_nc_deployment ? account.name.unwrap() : account.email.unwrap();
255-
const account_identifier_id = is_nc_deployment ? account._id : undefined;
255+
// Both NSFS NC and containerized will validate bucket policy against acccount id.
256+
const account_identifier_id = account._id;
256257
const account_identifier_arn = s3_bucket_policy_utils.get_bucket_policy_principal_arn(account);
257258

258259
// deny delete_bucket permissions from bucket_claim_owner accounts (accounts that were created by OBC from openshift\k8s)
@@ -296,7 +297,7 @@ async function authorize_request_policy(req) {
296297
let permission_by_id;
297298
let permission_by_name;
298299
let permission_by_arn;
299-
let permission_by_arn_owner;
300+
let permission_by_owner;
300301

301302
// In NC, we allow principal to be:
302303
// 1. account name (for backwards compatibility)
@@ -310,17 +311,18 @@ async function authorize_request_policy(req) {
310311
dbg.log3('authorize_request_policy: permission_by_id', permission_by_id);
311312
}
312313
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);
313-
if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
314+
// Check bucket policy permission against account name only for NSFS NC
315+
if (is_nc_deployment && permission_by_id !== "DENY" && account.owner === undefined) {
314316
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
315317
s3_policy, account_identifier_name, method, arn_path, req,
316318
{ disallow_public_access: public_access_block?.restrict_public_buckets }
317319
);
318320
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
319321
}
320322
if (permission_by_name === "DENY") throw new S3Error(S3Error.AccessDenied);
321-
// Containerized deployment always will have account_identifier_id undefined
323+
// Check bucket policy permission against ARN only for containerized deployments.
322324
// Policy permission is validated by account arn
323-
if (!account_identifier_id) {
325+
if (!is_nc_deployment && permission_by_id !== "DENY") {
324326
permission_by_arn = await s3_bucket_policy_utils.has_bucket_policy_permission(
325327
s3_policy, account_identifier_arn, method, arn_path, req,
326328
{ disallow_public_access: public_access_block?.restrict_public_buckets }
@@ -329,19 +331,20 @@ async function authorize_request_policy(req) {
329331
}
330332
if (permission_by_arn === "DENY") throw new S3Error(S3Error.AccessDenied);
331333

332-
// ARN check for users under the account
334+
// ARN and ID check for users under the account
333335
// ARN check is not implemented in NC yet
334336
if (!is_nc_deployment && account.owner !== undefined) {
335-
const owner_account_identifier_arn = s3_bucket_policy_utils.create_arn_for_root(account.owner);
336-
permission_by_arn_owner = await s3_bucket_policy_utils.has_bucket_policy_permission(
337-
s3_policy, owner_account_identifier_arn, method, arn_path, req,
337+
const owner_account_id = get_owner_account_id(account);
338+
const owner_account_identifier_arn = s3_bucket_policy_utils.create_arn_for_root(owner_account_id);
339+
permission_by_owner = await s3_bucket_policy_utils.has_bucket_policy_permission(
340+
s3_policy, [owner_account_identifier_arn, owner_account_id], method, arn_path, req,
338341
{ disallow_public_access: public_access_block?.restrict_public_buckets }
339342
);
340-
dbg.log3('authorize_request_policy permission_by_arn_owner', permission_by_arn_owner);
341-
if (permission_by_arn_owner === "DENY") throw new S3Error(S3Error.AccessDenied);
343+
dbg.log3('authorize_request_policy permission_by_arn_owner', permission_by_owner);
344+
if (permission_by_owner === "DENY") throw new S3Error(S3Error.AccessDenied);
342345
}
343346
if ((permission_by_id === "ALLOW" || permission_by_name === "ALLOW" ||
344-
permission_by_arn === "ALLOW" || permission_by_arn_owner === "ALLOW") || is_owner) return;
347+
permission_by_arn === "ALLOW" || permission_by_owner === "ALLOW") || is_owner) return;
345348

346349
throw new S3Error(S3Error.AccessDenied);
347350
}

src/server/common_services/auth_server.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,10 @@ async function has_bucket_action_permission(bucket, account, action, req_query,
629629
throw new Error('has_bucket_action_permission: action is required');
630630
}
631631
const arn = account.owner ? iam_utils.create_arn_for_user(account.owner._id.toString(), account.name.unwrap().split(':')[0], account.iam_path) :
632-
iam_utils.create_arn_for_root(account._id);
632+
iam_utils.create_arn_for_root(account._id.toString());
633633
const result = await s3_bucket_policy_utils.has_bucket_policy_permission(
634634
bucket_policy,
635-
arn,
635+
[arn, account._id.toString()],
636636
action,
637637
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
638638
req_query
@@ -641,11 +641,16 @@ async function has_bucket_action_permission(bucket, account, action, req_query,
641641
if (result === 'DENY') return false;
642642

643643
let permission_by_arn_owner;
644+
// Added to verify the IAM users's owner account have bucket access
645+
// If yes, IAM user also should get access
644646
if (account.owner) {
645-
const owner_account_identifier_arn = s3_bucket_policy_utils.create_arn_for_root(account.owner._id.toString());
647+
const owner_account_id = iam_utils.get_owner_account_id(account);
648+
const owner_account_identifier_arn = s3_bucket_policy_utils.create_arn_for_root(owner_account_id);
649+
// We support both ARN and account/user ID in bucket policy Principal, So if the bucket policy have only ARN
650+
// sharing array of ARN and ID can fix the issue that misses the access just because of bucket policy have ID as principal
646651
permission_by_arn_owner = await s3_bucket_policy_utils.has_bucket_policy_permission(
647652
bucket_policy,
648-
owner_account_identifier_arn,
653+
[owner_account_identifier_arn, owner_account_id],
649654
action,
650655
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
651656
req_query,

src/server/system_services/bucket_server.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,10 +523,9 @@ async function get_bucket_policy(req) {
523523
eg: arn:aws:iam::${account_id}:user/${iam_path}/${user_name}
524524
account email = ${iam_user_name}:${account_id}
525525
526-
@param {SensitiveString | String} principal Bucket policy principal
526+
@param {String} principal_as_string Bucket policy principal string
527527
*/
528-
async function get_account_by_principal(principal) {
529-
const principal_as_string = principal instanceof SensitiveString ? principal.unwrap() : principal;
528+
async function account_exists_by_principal_arn(principal_as_string) {
530529
const root_sufix = 'root';
531530
const user_sufix = 'user';
532531
const arn_parts = principal_as_string.split(':');
@@ -546,6 +545,27 @@ async function get_account_by_principal(principal) {
546545
//}
547546
}
548547

548+
/**
549+
Validate and return account by principal ARN and account id.
550+
551+
@param {SensitiveString | String} principal Bucket policy principal
552+
*/
553+
async function get_account_by_principal(principal) {
554+
const principal_as_string = principal instanceof SensitiveString ? principal.unwrap() : principal;
555+
const is_principal_arn = principal_as_string.startsWith('arn:aws:iam::');
556+
if (is_principal_arn) {
557+
const principal_by_arn = await account_exists_by_principal_arn(principal_as_string);
558+
dbg.log3('get_account_by_principal: principal_by_arn', principal_by_arn);
559+
if (principal_by_arn) return true;
560+
} else {
561+
const account = system_store.data.accounts.find(acc => acc._id.toString() === principal_as_string);
562+
const principal_by_id = account !== undefined;
563+
dbg.log3('get_account_by_principal: principal_by_id', principal_by_id);
564+
if (principal_by_id) return true;
565+
}
566+
return false;
567+
}
568+
549569
async function put_bucket_policy(req) {
550570
dbg.log0('put_bucket_policy:', req.rpc_params);
551571
const bucket = find_bucket(req, req.rpc_params.name);

src/test/integration_tests/api/iam/test_iam_basic_integration.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ mocha.describe('IAM basic integration tests - happy path', async function() {
7373
});
7474

7575
mocha.after(async () => {
76-
fs_utils.folder_delete(`${config_root}`);
76+
if (is_nc_coretest) {
77+
fs_utils.folder_delete(`${config_root}`);
78+
}
7779
});
7880

7981
mocha.describe('IAM User API', async function() {

0 commit comments

Comments
 (0)