Skip to content

Commit 0020baa

Browse files
committed
IAM | Principal validation and S3 permission updated with ID
Signed-off-by: Naveen Paul <napaul@redhat.com>
1 parent 8204d2a commit 0020baa

File tree

7 files changed

+76
-45
lines changed

7 files changed

+76
-45
lines changed

src/endpoint/s3/s3_bucket_policy_utils.js

Lines changed: 19 additions & 9 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

src/endpoint/s3/s3_rest.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ async function authorize_request_policy(req) {
252252
const account = req.object_sdk.requesting_account;
253253
const is_nc_deployment = 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)
@@ -309,17 +310,18 @@ async function authorize_request_policy(req) {
309310
dbg.log3('authorize_request_policy: permission_by_id', permission_by_id);
310311
}
311312
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);
312-
if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
313+
// Check bucket policy permission against account name only for NSFS NC
314+
if (is_nc_deployment && permission_by_id !== "DENY" && account.owner === undefined) {
313315
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
314316
s3_policy, account_identifier_name, method, arn_path, req,
315317
{ disallow_public_access: public_access_block?.restrict_public_buckets }
316318
);
317319
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
318320
}
319321
if (permission_by_name === "DENY") throw new S3Error(S3Error.AccessDenied);
320-
// Containerized deployment always will have account_identifier_id undefined
322+
// Check bucket policy permission against ARN only for containerized deployments.
321323
// Policy permission is validated by account arn
322-
if (!account_identifier_id) {
324+
if (!is_nc_deployment && permission_by_id !== "DENY") {
323325
permission_by_arn = await s3_bucket_policy_utils.has_bucket_policy_permission(
324326
s3_policy, account_identifier_arn, method, arn_path, req, public_access_block?.restrict_public_buckets
325327
);

src/server/common_services/auth_server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ async function has_bucket_action_permission(bucket, account, action, req_query,
559559
iam_utils.create_arn_for_root(account._id);
560560
const result = await s3_bucket_policy_utils.has_bucket_policy_permission(
561561
bucket_policy,
562-
arn,
562+
[arn, account._id.toString()],
563563
action,
564564
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
565565
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() {

src/test/integration_tests/api/s3/test_s3_bucket_policy.js

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ const cross_test_store = {};
6565
let user_a_account_details;
6666
let user_b_account_details;
6767

68+
let user_a_account_id;
69+
let user_b_account_id;
70+
6871
let admin_info;
69-
let account_info_a;
70-
let account_info_b;
7172

7273
let a_principal;
7374
let b_principal;
@@ -123,14 +124,14 @@ async function setup() {
123124
account.name = user_a;
124125
account.email = user_a;
125126
user_a_account_details = await rpc_client.account.create_account(account);
126-
account_info_a = user_a_account_details._id ? user_a_account_details : await rpc_client.account.read_account({ email: user_a });
127-
console.log('user_a_account_details', account_info_a);
127+
console.log('user_a_account_details', user_a_account_details);
128128
const user_a_keys = user_a_account_details.access_keys;
129+
user_a_account_id = is_nc_coretest ? user_a_account_details._id : user_a_account_details.id;
129130
account.name = user_b;
130131
account.email = user_b;
131132
user_b_account_details = await rpc_client.account.create_account(account);
132-
account_info_b = user_b_account_details._id ? user_b_account_details : await rpc_client.account.read_account({ email: user_b });
133-
console.log('user_b_account_details', account_info_b);
133+
console.log('user_b_account_details', user_b_account_details);
134+
user_b_account_id = is_nc_coretest ? user_b_account_details._id : user_b_account_details.id;
134135
const user_b_keys = user_b_account_details.access_keys;
135136
s3_creds.credentials = {
136137
accessKeyId: user_a_keys[0].access_key.unwrap(),
@@ -149,11 +150,11 @@ async function setup() {
149150
};
150151
/*
151152
For coretest nc, principal will have account name and
152-
+ for containerized deployment principal is ARN
153+
for containerized deployment principal is ARN
153154
*/
154155
admin_principal = is_nc_coretest ? EMAIL : s3_bucket_policy_utils.create_arn_for_root(admin_info._id.toString());
155-
a_principal = is_nc_coretest ? user_a : s3_bucket_policy_utils.create_arn_for_root(account_info_a._id.toString());
156-
b_principal = is_nc_coretest ? user_b : s3_bucket_policy_utils.create_arn_for_root(account_info_b._id.toString());
156+
a_principal = is_nc_coretest ? user_a : s3_bucket_policy_utils.create_arn_for_root(user_a_account_details.id.toString());
157+
b_principal = is_nc_coretest ? user_b : s3_bucket_policy_utils.create_arn_for_root(user_b_account_details.id.toString());
157158

158159
s3_owner = new S3(s3_creds);
159160
await s3_owner.createBucket({ Bucket: BKT });
@@ -312,12 +313,11 @@ mocha.describe('s3_bucket_policy', function() {
312313
});
313314

314315
mocha.it('should put/get bucket policy - principal by account ID', async function() {
315-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
316316
const policy = {
317317
Statement: [{
318-
Sid: `Allow all s3 actions on bucket ${BKT} to principal (by ID) ${user_a_account_details._id}`,
318+
Sid: `Allow all s3 actions on bucket ${BKT} to principal (by ID) ${user_a_account_id}`,
319319
Effect: 'Allow',
320-
Principal: { AWS: user_a_account_details._id },
320+
Principal: { AWS: user_a_account_id },
321321
Action: ['s3:*'],
322322
Resource: [`arn:aws:s3:::${BKT}`, `arn:aws:s3:::${BKT}/*`]
323323
}]
@@ -335,12 +335,11 @@ mocha.describe('s3_bucket_policy', function() {
335335
});
336336

337337
mocha.it('should put object to permitted account (bucket policy - principal by account ID', async function() {
338-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
339338
const policy = {
340339
Statement: [{
341-
Sid: `Allow all s3 actions on bucket ${BKT} to principal (by ID) ${user_a_account_details._id}`,
340+
Sid: `Allow all s3 actions on bucket ${BKT} to principal (by ID) ${user_a_account_id}`,
342341
Effect: 'Allow',
343-
Principal: { AWS: user_a_account_details._id },
342+
Principal: { AWS: user_a_account_id },
344343
Action: ['s3:*'],
345344
Resource: [`arn:aws:s3:::${BKT}`, `arn:aws:s3:::${BKT}/*`]
346345
}]
@@ -395,7 +394,7 @@ mocha.describe('s3_bucket_policy', function() {
395394
};
396395
}
397396
// Losing this value in-between, assigning it again
398-
a_principal = is_nc_coretest ? user_a : s3_bucket_policy_utils.create_arn_for_root(account_info_a._id.toString());
397+
a_principal = is_nc_coretest ? user_a : s3_bucket_policy_utils.create_arn_for_root(user_b_account_id.toString());
399398
const deny_account_by_name_all_s3_actions_statement = {
400399
Sid: `Do not allow user ${user_a} any s3 action`,
401400
Effect: 'Deny',
@@ -407,9 +406,8 @@ mocha.describe('s3_bucket_policy', function() {
407406
mocha.it('should not allow principal get object bucket policy with 2 statements: ' +
408407
'(1) DENY principal by account ID (2) ALLOW all principals as *', async function() {
409408
// in NC we allow principal to be also IDs
410-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
411409
const deny_account_by_id_all_s3_actions_statement =
412-
get_deny_account_by_id_all_s3_actions_statement(user_a_account_details._id);
410+
get_deny_account_by_id_all_s3_actions_statement(user_a_account_id);
413411
const policy = {
414412
Statement: [
415413
allow_all_principals_all_s3_actions_statement,
@@ -481,9 +479,9 @@ mocha.describe('s3_bucket_policy', function() {
481479
mocha.it('should not allow principal get object bucket policy with 2 statements: ' +
482480
'(1) DENY principal by account ID (2) ALLOW by account name', async function() {
483481
// in NC we allow principal to be also IDs
484-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
482+
// if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
485483
const deny_account_by_id_all_s3_actions_statement =
486-
get_deny_account_by_id_all_s3_actions_statement(user_a_account_details._id);
484+
get_deny_account_by_id_all_s3_actions_statement(user_a_account_id);
487485
const allow_account_by_name_all_s3_actions_statement = _.cloneDeep(deny_account_by_name_all_s3_actions_statement);
488486
allow_account_by_name_all_s3_actions_statement.Effect = 'Allow';
489487
allow_account_by_name_all_s3_actions_statement.Sid = `Allow user ${user_a} any s3 action`;
@@ -516,12 +514,12 @@ mocha.describe('s3_bucket_policy', function() {
516514
mocha.it('should not allow principal get object bucket policy with 2 statements: ' +
517515
'(1) DENY principal by account name (2) ALLOW by account ID', async function() {
518516
// in NC we allow principal to be also IDs
519-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
517+
// if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
520518
const deny_account_by_id_all_s3_actions_statement =
521-
get_deny_account_by_id_all_s3_actions_statement(user_a_account_details._id);
519+
get_deny_account_by_id_all_s3_actions_statement(user_a_account_id);
522520
const allow_account_by_id_all_s3_actions_statement = _.cloneDeep(deny_account_by_id_all_s3_actions_statement);
523521
allow_account_by_id_all_s3_actions_statement.Effect = 'Allow';
524-
allow_account_by_id_all_s3_actions_statement.Sid = `Allow user ${user_a_account_details._id} any s3 action`;
522+
allow_account_by_id_all_s3_actions_statement.Sid = `Allow user ${user_a_account_id} any s3 action`;
525523
const policy = {
526524
Statement: [
527525
deny_account_by_name_all_s3_actions_statement,
@@ -582,12 +580,12 @@ mocha.describe('s3_bucket_policy', function() {
582580
mocha.it('should not allow principal get object bucket policy with 2 statements: ' +
583581
'(1) ALLOW principal by account ID (2) DENY all principals as * (specific action only)', async function() {
584582
// in NC we allow principal to be also IDs
585-
if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
583+
// if (!is_nc_coretest) this.skip(); // eslint-disable-line no-invalid-this
586584
const deny_account_by_id_all_s3_actions_statement =
587-
get_deny_account_by_id_all_s3_actions_statement(user_a_account_details._id);
585+
get_deny_account_by_id_all_s3_actions_statement(user_a_account_id);
588586
const allow_account_by_id_all_s3_actions_statement = _.cloneDeep(deny_account_by_id_all_s3_actions_statement);
589587
allow_account_by_id_all_s3_actions_statement.Effect = 'Allow';
590-
allow_account_by_id_all_s3_actions_statement.Sid = `Allow user ${user_a_account_details._id} any s3 action`;
588+
allow_account_by_id_all_s3_actions_statement.Sid = `Allow user ${user_a_account_id} any s3 action`;
591589
const policy = {
592590
Statement: [
593591
allow_account_by_id_all_s3_actions_statement,

src/util/account_util.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,6 @@ function _check_root_account_owns_user(root_account, user_account) {
342342
return root_account_id === owner_account_id;
343343
}
344344

345-
346345
function _check_if_requesting_account_is_root_account(action, requesting_account, user_details = {}) {
347346
const is_root_account = _check_root_account(requesting_account);
348347
dbg.log1(`AccountSpaceNB.${action} requesting_account ID: ${requesting_account._id}` +

0 commit comments

Comments
 (0)