Skip to content

Commit c5026ab

Browse files
authored
Merge pull request #8175 from shirady/nsfs-iam-account-implicit-policy
NSFS | NC | IAM Service - Accounts Permission When No Bucket Policy
2 parents d0b9dbf + c65e20d commit c5026ab

File tree

6 files changed

+143
-6
lines changed

6 files changed

+143
-6
lines changed

docs/design/iam.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ Source: AccessKeys
8989
- root account
9090
- all IAM users only for themselves (except the first creation that can be done only by the root account).
9191

92+
### No Bucket Policy
93+
If the resource doesn’t have a bucket policy the IAM user accounts can have access to the resources of the same root account.
94+
For example:
95+
- root account creates 2 users (both are owned by it): user1, user2 and a bucket (bucket owner: <root-account-id>, bucket creator: <account-id-user1>).
96+
- user1 upload a file to the bucket
97+
- user2 can delete this bucket (after it is empty): although user2 is not the creator, without a bucket policy his root account is the owner so he can delete the bucket.
98+
Note: Currently, we do not allow users to create a bucket.
99+
92100
### Root Accounts Manager
93101
The root accounts managers are a solution for creating root accounts using the IAM API.
94102

src/endpoint/s3/s3_rest.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ async function authorize_request_policy(req) {
216216
if (!req.params.bucket) return;
217217
if (req.op_name === 'put_bucket') return;
218218

219+
// owner_account is { id: bucket.owner_account, email: bucket.bucket_owner };
219220
const { s3_policy, system_owner, bucket_owner, owner_account } = await req.object_sdk.read_bucket_sdk_policy_info(req.params.bucket);
220221
const auth_token = req.object_sdk.get_auth_token();
221222
const arn_path = _get_arn_from_req_path(req);
@@ -236,13 +237,17 @@ async function authorize_request_policy(req) {
236237

237238
const is_owner = (function() {
238239
if (account.bucket_claim_owner && account.bucket_claim_owner.unwrap() === req.params.bucket) return true;
239-
if (req.object_sdk.nsfs_config_root && account._id === owner_account.id) return true; // NC NSFS case
240+
if (owner_account && owner_account.id === account._id) return true;
240241
if (account_identifier === bucket_owner.unwrap()) return true;
241242
return false;
242243
}());
243244

244245
if (!s3_policy) {
245-
if (is_owner) return;
246+
// in case we do not have bucket policy
247+
// we allow IAM account to access a bucket that is owned by their root account
248+
const is_iam_account_and_same_root_account_owner = account.owner !== undefined &&
249+
owner_account && account.owner === owner_account.id;
250+
if (is_owner || is_iam_account_and_same_root_account_owner) return;
246251
throw new S3Error(S3Error.AccessDenied);
247252
}
248253
const permission = await s3_bucket_policy_utils.has_bucket_policy_permission(

src/sdk/bucketspace_fs.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
265265
if (!sdk.requesting_account.allow_bucket_creation) {
266266
throw new RpcError('UNAUTHORIZED', 'Not allowed to create new buckets');
267267
}
268+
// currently we do not allow IAM account to create a bucket (temporary)
269+
if (sdk.requesting_account.owner !== undefined) {
270+
dbg.warn('create_bucket: account is IAM account (currently not allowed to create buckets)');
271+
throw new RpcError('UNAUTHORIZED', 'Not allowed to create new buckets');
272+
}
268273
if (!sdk.requesting_account.nsfs_account_config || !sdk.requesting_account.nsfs_account_config.new_buckets_path) {
269274
throw new RpcError('MISSING_NSFS_ACCOUNT_CONFIGURATION');
270275
}
@@ -320,6 +325,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
320325
name,
321326
tag: js_utils.default_value(tag, undefined),
322327
owner_account: account._id,
328+
creator: account._id,
323329
system_owner: new SensitiveString(account.name),
324330
bucket_owner: new SensitiveString(account.name),
325331
versioning: config.NSFS_VERSIONING_ENABLED && lock_enabled ? 'ENABLED' : 'DISABLED',
@@ -746,10 +752,16 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
746752

747753
// If the system owner account wants to access the bucket, allow it
748754
if (is_system_owner) return true;
749-
const is_owner = (bucket.bucket_owner.unwrap() === account_identifier);
755+
const is_owner = (bucket.owner_account && bucket.owner_account.id === account._id);
750756
const bucket_policy = bucket.s3_policy;
751757

752-
if (!bucket_policy) return is_owner;
758+
if (!bucket_policy) {
759+
// in case we do not have bucket policy
760+
// we allow IAM account to access a bucket that that is owned by their root account
761+
const is_iam_and_same_root_account_owner = account.owner !== undefined &&
762+
account.owner === bucket.owner_account.id;
763+
return is_owner || is_iam_and_same_root_account_owner;
764+
}
753765
if (!action) {
754766
throw new Error('has_bucket_action_permission: action is required');
755767
}

src/server/system_services/schemas/nsfs_bucket_schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ module.exports = {
2323
owner_account: {
2424
type: 'string',
2525
},
26+
// creator is the account id that created this bucket (internal information)
27+
creator: {
28+
type: 'string',
29+
},
2630
name: {
2731
type: 'string',
2832
},

src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_schema_validation.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ describe('schema validation NC NSFS bucket', () => {
100100
nsfs_schema_utils.validate_bucket_schema(bucket_data);
101101
});
102102

103+
it('nsfs_bucket with creator', () => {
104+
const bucket_data = get_bucket_data();
105+
bucket_data.creator = '65a62e22ceae5e5f1a758ab1';
106+
nsfs_schema_utils.validate_bucket_schema(bucket_data);
107+
});
103108
});
104109

105110
describe('bucket with additional properties', () => {
@@ -434,6 +439,14 @@ describe('schema validation NC NSFS bucket', () => {
434439
assert_validation(bucket_data, reason, message);
435440
});
436441

442+
it('bucket with creator as a number (instead of string)', () => {
443+
const bucket_data = get_bucket_data();
444+
bucket_data.creator = 123; // number instead of string
445+
const reason = 'Test should have failed because of wrong type for' +
446+
'creator with number (instead of string)';
447+
const message = 'must be string';
448+
assert_validation(bucket_data, reason, message);
449+
});
437450
});
438451

439452
describe('skip schema check by config test', () => {

src/test/unit_tests/test_bucketspace_fs.js

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const test_bucket = 'bucket1';
2525
const test_not_empty_bucket = 'notemptybucket';
2626
const test_bucket_temp_dir = 'buckettempdir';
2727
const test_bucket_invalid = 'bucket_invalid';
28+
const test_bucket_iam_account = 'bucket-iam-account-can-access';
2829

2930
const tmp_fs_path = path.join(TMP_PATH, 'test_bucketspace_fs');
3031
const config_root = path.join(tmp_fs_path, 'config_root');
@@ -191,6 +192,57 @@ function make_dummy_object_sdk() {
191192
}
192193
};
193194
}
195+
196+
// account_user2 (copied from the dummy sdk)
197+
// is the root account of account_iam_user1
198+
const account_iam_user1 = {
199+
_id: '65a8edc9bc5d5bbf9db71b94',
200+
name: 'iam_user_1',
201+
202+
owner: dummy_object_sdk.requesting_account._id,
203+
allow_bucket_creation: dummy_object_sdk.requesting_account.allow_bucket_creation,
204+
access_keys: [{
205+
access_key: 'a-abcdefghijklmn123459',
206+
secret_key: 's-abcdefghijklmn123459Example'
207+
}],
208+
nsfs_account_config: {
209+
uid: dummy_object_sdk.requesting_account.nsfs_account_config.uid,
210+
gid: dummy_object_sdk.requesting_account.nsfs_account_config.gid,
211+
new_buckets_path: dummy_object_sdk.requesting_account.nsfs_account_config.new_buckets_path
212+
},
213+
creation_date: '2023-11-30T04:46:33.815Z',
214+
};
215+
216+
// account_user2 (copied from the dummy sdk)
217+
// is the root account of account_iam_user2
218+
const account_iam_user2 = {
219+
_id: '65a8edc9bc5d5bbf9db71b95',
220+
name: 'iam_user_2',
221+
222+
owner: dummy_object_sdk.requesting_account._id,
223+
allow_bucket_creation: dummy_object_sdk.requesting_account.allow_bucket_creation,
224+
access_keys: [{
225+
access_key: 'a-abcdefghijklmn123460',
226+
secret_key: 's-abcdefghijklmn123460Example'
227+
}],
228+
nsfs_account_config: {
229+
uid: dummy_object_sdk.requesting_account.nsfs_account_config.uid,
230+
gid: dummy_object_sdk.requesting_account.nsfs_account_config.gid,
231+
new_buckets_path: dummy_object_sdk.requesting_account.nsfs_account_config.new_buckets_path
232+
},
233+
creation_date: '2023-12-30T04:46:33.815Z',
234+
};
235+
236+
function make_dummy_object_sdk_for_account(dummy_object_sdk_to_copy, account) {
237+
const dummy_object_sdk_for_account = _.cloneDeep(dummy_object_sdk_to_copy);
238+
dummy_object_sdk_for_account.requesting_account = account;
239+
dummy_object_sdk_for_account.requesting_account.name = new SensitiveString(
240+
dummy_object_sdk_for_account.requesting_account.name);
241+
dummy_object_sdk_for_account.requesting_account.email = new SensitiveString(
242+
dummy_object_sdk_for_account.requesting_account.email);
243+
return dummy_object_sdk_for_account;
244+
}
245+
194246
function make_invalid_dummy_object_sdk() {
195247
return {
196248
requesting_account: {
@@ -216,7 +268,7 @@ mocha.describe('bucketspace_fs', function() {
216268
await fs_utils.create_fresh_path(`${config_root}/${dir}`))
217269
);
218270
await fs_utils.create_fresh_path(new_buckets_path);
219-
for (let account of [account_user1, account_user2, account_user3]) {
271+
for (let account of [account_user1, account_user2, account_user3, account_iam_user1, account_iam_user2]) {
220272
account = await nc_mkm.encrypt_access_keys(account);
221273
const account_path = get_config_file_path(CONFIG_SUBDIRS.ACCOUNTS, account.name);
222274
const account_access_path = get_access_key_symlink_path(CONFIG_SUBDIRS.ACCESS_KEYS, account.access_keys[0].access_key);
@@ -312,9 +364,23 @@ mocha.describe('bucketspace_fs', function() {
312364
assert.ok(err.rpc_code === 'UNAUTHORIZED');
313365
}
314366
});
367+
mocha.it('should fail - create bucket by iam account', async function() {
368+
// currently we do not allow IAM accounts to create buckets
369+
try {
370+
const param = { name: test_bucket_iam_account};
371+
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1);
372+
await bucketspace_fs.create_bucket(param, dummy_object_sdk_for_iam_account);
373+
assert.fail('should have failed with UNAUTHORIZED bucket creation');
374+
} catch (err) {
375+
assert.ok(err.rpc_code === 'UNAUTHORIZED');
376+
}
377+
});
315378
mocha.after(async function() {
316379
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket}`);
317-
const file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket);
380+
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket_iam_account}`);
381+
let file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket);
382+
await fs_utils.file_delete(file_path);
383+
file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket_iam_account);
318384
await fs_utils.file_delete(file_path);
319385
});
320386
});
@@ -333,6 +399,19 @@ mocha.describe('bucketspace_fs', function() {
333399
const objects = await bucketspace_fs.list_buckets(dummy_object_sdk);
334400
assert.equal(objects.buckets.length, 1);
335401
});
402+
mocha.it('list buckets - iam accounts', async function() {
403+
// root account created a bucket
404+
405+
// account_iam_user2 can list the created bucket (the implicit policy - same root account)
406+
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1);
407+
const res = await bucketspace_fs.list_buckets(dummy_object_sdk_for_iam_account);
408+
assert.equal(res.buckets.length, 1);
409+
410+
// account_iam_user2 can list the created bucket (the implicit policy - same root account)
411+
const dummy_object_sdk_for_iam_account2 = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user2);
412+
const res2 = await bucketspace_fs.list_buckets(dummy_object_sdk_for_iam_account2);
413+
assert.equal(res2.buckets.length, 1);
414+
});
336415
mocha.after(async function() {
337416
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket}`);
338417
const file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket);
@@ -413,6 +492,22 @@ mocha.describe('bucketspace_fs', function() {
413492
}
414493
await fs.promises.stat(path.join(new_buckets_path, param.name));
415494
});
495+
496+
mocha.it('delete buckets - iam accounts (another IAM account deletes the bucket)', async function() {
497+
// root account created the bucket
498+
await create_bucket(test_bucket_iam_account);
499+
500+
// account_iam_user1 can see the bucket in the list
501+
const dummy_object_sdk_for_account_iam_user1 = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1);
502+
const res = await bucketspace_fs.list_buckets(dummy_object_sdk_for_account_iam_user1);
503+
assert.ok(res.buckets.length > 0);
504+
assert.ok(res.buckets.some(bucket => bucket.name.unwrap() === test_bucket_iam_account));
505+
506+
const param = { name: test_bucket_iam_account};
507+
// account_iam_user2 can delete the created bucket (the implicit policy - same root account)
508+
const dummy_object_sdk_for_account_iam_user2 = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user2);
509+
await bucketspace_fs.delete_bucket(param, dummy_object_sdk_for_account_iam_user2);
510+
});
416511
});
417512
mocha.describe('set_bucket_versioning', function() {
418513
mocha.before(async function() {

0 commit comments

Comments
 (0)