Skip to content

Commit c65e20d

Browse files
committed
NSFS | NC | IAM Service - Accounts Permission When No Bucket Policy
1. Add more properties to nsfs_bucket_schema (not required) and update the new_bucket_defaults in BucketSpaceFS: - creator = creator is the account ID that created this bucket (internal information). Notes: Currently we do not allow IAM accounts to create a bucket (temporary), it will be changed after we have the config structure, therefore in the future, we could see the IAM account ID in the creator property. 2. Change the condition in authorize_request_policy (in s3_rest) and has_bucket_action_permission (in bucketspace_fs) for the same root account (alternative for ownership when there is no bucket policy). Those next changes are not related to IAM, but were raised as a part of the code review: 3. In authorize_request_policy (in s3_rest) remove the condition req.object_sdk.nsfs_config_root from the owner condition. 4. In has_bucket_action_permission (in bucketspace_fs) change the condition of is_owner from account name to id. Signed-off-by: shirady <[email protected]>
1 parent d0b9dbf commit c65e20d

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)