Skip to content

Commit 22650de

Browse files
committed
[MCG NC] Bucket policy with deny statement using NotPrincipal also denies the principal
This is the put-bucket-policy case for combination of Effect : DENY, Action: $OPERATION, NotPrincipal: $ACCOUNT The $ACCOUNT mentioned in the "NotPrincipal" should be excluded from DENy operation and should be allowed on the $OPERATION Example: For operation get_object, if we have DENY effect for * (all accounts) and we want to give access to any one or few accounts, then that account can be part of "NotPrincipal" Fixes: https://issues.redhat.com/browse/DFBUGS-1519 Signed-off-by: Vinayakswami Hariharmath <[email protected]>
1 parent 7681865 commit 22650de

File tree

4 files changed

+74
-12
lines changed

4 files changed

+74
-12
lines changed

src/endpoint/s3/s3_bucket_policy_utils.js

+38-10
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,31 @@ async function _is_object_version_fit(req, predicate, value) {
146146
return res;
147147
}
148148

149-
async function has_bucket_policy_permission(policy, account, method, arn_path, req) {
149+
async function has_bucket_policy_permission(policy, account, method, arn_path, req, account_identifier_type = 'id') {
150150
const [allow_statements, deny_statements] = _.partition(policy.Statement, statement => statement.Effect === 'Allow');
151151

152152
// the case where the permission is an array started in op get_object_attributes
153153
const method_arr = Array.isArray(method) ? method : [method];
154154

155155
// look for explicit denies
156-
const res_arr_deny = await is_statement_fit_of_method_array(deny_statements, account, method_arr, arn_path, req);
156+
const res_arr_deny = await is_statement_fit_of_method_array(deny_statements,
157+
account,
158+
method_arr,
159+
arn_path,
160+
req,
161+
account_identifier_type
162+
);
157163
if (res_arr_deny.every(item => item)) return 'DENY';
158164

159165
// look for explicit allows
160-
const res_arr_allow = await is_statement_fit_of_method_array(allow_statements, account, method_arr, arn_path, req);
166+
const res_arr_allow = await is_statement_fit_of_method_array(
167+
allow_statements,
168+
account,
169+
method_arr,
170+
arn_path,
171+
req,
172+
account_identifier_type
173+
);
161174
if (res_arr_allow.every(item => item)) return 'ALLOW';
162175

163176
// implicit deny
@@ -177,9 +190,8 @@ function _is_action_fit(method, statement) {
177190
return statement.Action ? action_fit : !action_fit;
178191
}
179192

180-
function _is_principal_fit(account, statement) {
193+
function _is_principal_fit(account, statement, account_identifier_type) {
181194
let statement_principal = statement.Principal || statement.NotPrincipal;
182-
183195
let principal_fit = false;
184196
statement_principal = statement_principal.AWS ? statement_principal.AWS : statement_principal;
185197
for (const principal of _.flatten([statement_principal])) {
@@ -189,7 +201,23 @@ function _is_principal_fit(account, statement) {
189201
break;
190202
}
191203
}
192-
return statement.Principal ? principal_fit : !principal_fit;
204+
205+
if (statement.NotPrincipal) {
206+
// If the account is categorized as "Not Principal," we need to ALLOW operations
207+
// for that account under the associated principal.
208+
//
209+
// To maintain backward compatibility, we also support name-based comparison for principal matching.
210+
//
211+
// - If `account_identifier_type` is "id" but the `account` parameter contains a name,
212+
// we return `false` to proceed/bypass to the next step, where we check for a name match.
213+
//
214+
// - In the next step, if `account_identifier_type` is "name" and the `account` also contains a name,
215+
// we return the actual value.
216+
217+
return account_identifier_type === 'id' ? principal_fit: !principal_fit;
218+
}
219+
220+
return principal_fit;
193221
}
194222

195223
function _is_resource_fit(arn_path, statement) {
@@ -207,15 +235,15 @@ function _is_resource_fit(arn_path, statement) {
207235
return statement.Resource ? resource_fit : !resource_fit;
208236
}
209237

210-
async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req) {
238+
async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req, account_identifier_type) {
211239
return Promise.all(method_arr.map(method_permission =>
212-
_is_statements_fit(statements, account, method_permission, arn_path, req)));
240+
_is_statements_fit(statements, account, method_permission, arn_path, req, account_identifier_type)));
213241
}
214242

215-
async function _is_statements_fit(statements, account, method, arn_path, req) {
243+
async function _is_statements_fit(statements, account, method, arn_path, req, account_identifier_type) {
216244
for (const statement of statements) {
217245
const action_fit = _is_action_fit(method, statement);
218-
const principal_fit = _is_principal_fit(account, statement);
246+
const principal_fit = _is_principal_fit(account, statement, account_identifier_type);
219247
const resource_fit = _is_resource_fit(arn_path, statement);
220248
const condition_fit = await _is_condition_fit(statement, req, method);
221249

src/endpoint/s3/s3_rest.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,9 @@ async function authorize_request_policy(req) {
293293
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);
294294

295295
if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
296+
const account_identifier_type = 'name';
296297
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
297-
s3_policy, account_identifier_name, method, arn_path, req);
298+
s3_policy, account_identifier_name, method, arn_path, req, account_identifier_type);
298299
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
299300
}
300301
if (permission_by_name === "DENY") throw new S3Error(S3Error.AccessDenied);

src/sdk/bucketspace_fs.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -819,12 +819,14 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
819819
// we (currently) allow account identified to be both id and name,
820820
// so if by-id failed, try also name
821821
if (account.owner === undefined) {
822+
const account_identifier_type = 'name';
822823
permission_by_name = await bucket_policy_utils.has_bucket_policy_permission(
823824
bucket_policy,
824825
account.name.unwrap(),
825826
action,
826827
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
827-
undefined
828+
undefined,
829+
account_identifier_type
828830
);
829831
}
830832
if (permission_by_name === 'DENY') return false;

src/test/unit_tests/test_s3_bucket_policy.js

+31
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,37 @@ mocha.describe('s3_bucket_policy', function() {
16471647
}));
16481648
});
16491649

1650+
mocha.it.skip('should be able to use notPrincipal with Effect Deny', async function() {
1651+
const self = this; // eslint-disable-line no-invalid-this
1652+
self.timeout(15000);
1653+
const auth_put_policy = {
1654+
Version: '2012-10-17',
1655+
Statement: [
1656+
{
1657+
Effect: 'Deny',
1658+
NotPrincipal: { AWS: user_a },
1659+
Action: ['s3:PutObject'],
1660+
Resource: [`arn:aws:s3:::${BKT}/*`]
1661+
}
1662+
]};
1663+
await s3_owner.putBucketPolicy({
1664+
Bucket: BKT,
1665+
Policy: JSON.stringify(auth_put_policy)
1666+
});
1667+
1668+
await s3_b.putObject({
1669+
Body: BODY,
1670+
Bucket: BKT,
1671+
Key: KEY,
1672+
});
1673+
1674+
await assert_throws_async(s3_a.putObject({
1675+
Body: BODY,
1676+
Bucket: BKT,
1677+
Key: KEY,
1678+
}));
1679+
});
1680+
16501681
mocha.it('should be able to use notResource', async function() {
16511682
const self = this; // eslint-disable-line no-invalid-this
16521683
self.timeout(15000);

0 commit comments

Comments
 (0)