Skip to content

Commit ad24414

Browse files
authored
Merge pull request #7812 from romayalon/romy-fix-list-buckets-missing-policy-check
NSFS | NC | list buckets missing policy check & performance improvement
2 parents 2cc5486 + 553df8c commit ad24414

File tree

5 files changed

+425
-169
lines changed

5 files changed

+425
-169
lines changed

src/sdk/bucketspace_fs.js

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ const bucket_semaphore = new KeysSemaphore(1);
3232

3333
/**
3434
* @param {nb.ObjectSDK} object_sdk
35+
* @param {string} [fs_backend]
3536
* @returns {nb.NativeFSContext}
3637
*/
37-
function prepare_fs_context(object_sdk) {
38+
function prepare_fs_context(object_sdk, fs_backend) {
3839
const fs_context = object_sdk?.requesting_account?.nsfs_account_config;
3940
if (!fs_context) throw new RpcError('UNAUTHORIZED', 'nsfs_account_config is missing');
4041
fs_context.warn_threshold_ms = config.NSFS_WARN_THRESHOLD_MS;
42+
fs_context.backend = fs_backend;
4143
// TODO:
4244
//if (this.stats) fs_context.report_fs_stats = this.stats.update_fs_stats;
4345
return fs_context;
@@ -192,6 +194,18 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
192194
////////////
193195

194196
//TODO: we need to add pagination support to list buckets for more than 1000 buckets.
197+
/**
198+
* list_buckets will read all bucket config files, and filter them according to the requesting account's
199+
* permissions
200+
* a. First iteration - map_with_concurrency with concurrency rate of 10 entries at the same time.
201+
* a.1. if entry is dir file/entry is non json - we will return undefined
202+
* a.2. if bucket is unaccessible by bucket policy - we will return undefined
203+
* a.3. if underlying storage of the bucket is unaccessible by the account's uid/gid - we will return undefined
204+
* a.4. else - return the bucket config info.
205+
* b. Second iteration - filter empty entries - filter will remove undefined values produced by the map_with_concurrency().
206+
* @param {nb.ObjectSDK} object_sdk
207+
* @returns {Promise<object>}
208+
*/
195209
async list_buckets(object_sdk) {
196210
let entries;
197211
try {
@@ -203,55 +217,27 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
203217
}
204218
throw this._translate_bucket_error_codes(err);
205219
}
206-
// TODO - replace filter and map to map with concurrency
207-
const bucket_config_files = entries.filter(entree => !native_fs_utils.isDirectory(entree) && entree.name.endsWith('.json'));
208-
const bucket_names = bucket_config_files.map(bucket_config_file => this.get_bucket_name(bucket_config_file.name));
209-
return this.validate_bucket_access(bucket_names, object_sdk);
210-
}
211-
212-
async validate_bucket_access(buckets, object_sdk) {
213-
// TODO - replace map & filter to map with concurrency
214-
const has_access_buckets = (await P.all(_.map(
215-
buckets,
216-
async bucket => {
217-
dbg.log1('bucketspace_fs.validate_bucket_access:', bucket.name.unwrap());
218-
const bucket_config_info = await object_sdk.read_bucket_sdk_config_info(bucket.name.unwrap());
219-
const ns = bucket_config_info.namespace;
220-
const is_nsfs_bucket = object_sdk.is_nsfs_bucket(ns);
221-
const accessible = is_nsfs_bucket ? await this._has_access_to_nsfs_dir(ns, object_sdk) : false;
222-
dbg.log1('bucketspace_fs.validate_bucket_access:', bucket.name.unwrap(), is_nsfs_bucket, accessible);
223-
return accessible && { ...bucket, creation_date: bucket_config_info.creation_date };
224-
}))).filter(bucket_info => bucket_info);
225-
return { buckets: has_access_buckets };
226-
}
227220

228-
async _has_access_to_nsfs_dir(ns, object_sdk) {
229221
const account = object_sdk.requesting_account;
230-
dbg.log0('_has_access_to_nsfs_dir: nsr: ', ns, 'account.nsfs_account_config: ', account && account.nsfs_account_config);
231-
// nsfs bucket
232-
if (!account || !account.nsfs_account_config || _.isUndefined(account.nsfs_account_config.uid) ||
233-
_.isUndefined(account.nsfs_account_config.gid)) return false;
234-
try {
235-
dbg.log0('_has_access_to_nsfs_dir: checking access:', ns.write_resource, account.nsfs_account_config.uid, account.nsfs_account_config.gid);
236-
await nb_native().fs.checkAccess({
237-
uid: account.nsfs_account_config.uid,
238-
gid: account.nsfs_account_config.gid,
239-
backend: ns.write_resource.resource.fs_backend,
240-
warn_threshold_ms: config.NSFS_WARN_THRESHOLD_MS,
241-
}, path.join(ns.write_resource.resource.fs_root_path, ns.write_resource.path || ''));
242-
243-
return true;
244-
} catch (err) {
245-
dbg.log0('_has_access_to_nsfs_dir: failed', err);
246-
if (err.code === 'ENOENT' || err.code === 'EACCES' || (err.code === 'EPERM' && err.message === 'Operation not permitted')) return false;
247-
throw err;
248-
}
222+
const buckets = await P.map_with_concurrency(10, entries, async entry => {
223+
if (native_fs_utils.isDirectory(entry) || !entry.name.endsWith('.json')) {
224+
return;
225+
}
226+
const bucket_name = this.get_bucket_name(entry.name);
227+
const bucket = await object_sdk.read_bucket_sdk_config_info(bucket_name);
228+
const bucket_policy_accessible = await this.has_bucket_action_permission(bucket, account, 's3:ListBucket');
229+
if (!bucket_policy_accessible) return;
230+
const fs_accessible = await this.validate_fs_bucket_access(bucket, object_sdk);
231+
if (!fs_accessible) return;
232+
return bucket;
233+
});
234+
return { buckets: buckets.filter(bucket => bucket) };
249235
}
250236

251237
get_bucket_name(bucket_config_file_name) {
252238
let bucket_name = path.basename(bucket_config_file_name);
253239
bucket_name = bucket_name.slice(0, bucket_name.indexOf('.json'));
254-
return { name: new SensitiveString(bucket_name) };
240+
return bucket_name;
255241
}
256242

257243
async read_bucket(params) {
@@ -651,6 +637,68 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
651637
async put_object_lock_configuration(params, object_sdk) {
652638
// TODO
653639
}
640+
641+
/////////////////
642+
///// UTILS /////
643+
/////////////////
644+
645+
// TODO: move the following 3 functions - has_bucket_action_permission(), validate_fs_bucket_access(), _has_access_to_nsfs_dir()
646+
// so they can be re-used
647+
async has_bucket_action_permission(bucket, account, action, bucket_path = "") {
648+
const account_identifier = account.name.unwrap();
649+
dbg.log1('has_bucket_action_permission:', bucket.name.unwrap(), account_identifier, bucket.bucket_owner.unwrap());
650+
651+
const is_system_owner = account_identifier === bucket.system_owner.unwrap();
652+
653+
// If the system owner account wants to access the bucket, allow it
654+
if (is_system_owner) return true;
655+
const is_owner = (bucket.bucket_owner.unwrap() === account_identifier);
656+
const bucket_policy = bucket.s3_policy;
657+
658+
if (!bucket_policy) return is_owner;
659+
if (!action) {
660+
throw new Error('has_bucket_action_permission: action is required');
661+
}
662+
663+
const result = await bucket_policy_utils.has_bucket_policy_permission(
664+
bucket_policy,
665+
account_identifier,
666+
action,
667+
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
668+
undefined
669+
);
670+
671+
if (result === 'DENY') return false;
672+
return is_owner || result === 'ALLOW';
673+
}
674+
675+
async validate_fs_bucket_access(bucket, object_sdk) {
676+
dbg.log1('bucketspace_fs.validate_fs_bucket_access:', bucket.name.unwrap());
677+
const ns = bucket.namespace;
678+
const is_nsfs_bucket = object_sdk.is_nsfs_bucket(ns);
679+
const accessible = is_nsfs_bucket ? await this._has_access_to_nsfs_dir(ns, object_sdk) : false;
680+
dbg.log1('bucketspace_fs.validate_fs_bucket_access:', bucket.name.unwrap(), is_nsfs_bucket, accessible);
681+
return accessible;
682+
}
683+
684+
async _has_access_to_nsfs_dir(ns, object_sdk) {
685+
const account = object_sdk.requesting_account;
686+
dbg.log1('_has_access_to_nsfs_dir: nsr: ', ns, 'account.nsfs_account_config: ', account && account.nsfs_account_config);
687+
// nsfs bucket
688+
if (!account || !account.nsfs_account_config || _.isUndefined(account.nsfs_account_config.uid) ||
689+
_.isUndefined(account.nsfs_account_config.gid)) return false;
690+
try {
691+
dbg.log1('_has_access_to_nsfs_dir: checking access:', ns.write_resource, account.nsfs_account_config.uid, account.nsfs_account_config.gid);
692+
const path_to_check = path.join(ns.write_resource.resource.fs_root_path, ns.write_resource.path || '');
693+
const fs_context = prepare_fs_context(object_sdk, ns.write_resource.resource.fs_backend);
694+
await nb_native().fs.checkAccess(fs_context, path_to_check);
695+
return true;
696+
} catch (err) {
697+
dbg.log0('_has_access_to_nsfs_dir: failed', err);
698+
if (err.code === 'ENOENT' || err.code === 'EACCES' || (err.code === 'EPERM' && err.message === 'Operation not permitted')) return false;
699+
throw err;
700+
}
701+
}
654702
}
655703

656704
module.exports = BucketSpaceFS;

src/sdk/bucketspace_simple_fs.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ class BucketSpaceSimpleFS {
3636
////////////
3737
// BUCKET //
3838
////////////
39-
40-
async list_buckets() {
39+
/**
40+
* @param {nb.ObjectSDK} object_sdk
41+
* @returns {Promise<object>}
42+
*/
43+
async list_buckets(object_sdk) {
4144
try {
4245
const entries = await nb_native().fs.readdir(this.fs_context, this.fs_root);
4346
const dirs_only = entries.filter(entree => native_fs_utils.isDirectory(entree));

src/test/unit_tests/nc_coretest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ function create_namespace_resource_mock(options) {
375375
async function create_bucket_manage(options) {
376376
const { resource, path } = options.namespace.write_resource;
377377
const bucket_storage_path = p.join(nsrs_to_root_paths[resource], path);
378-
const cli_options = { name: options.name, owner: NC_CORETEST, path: bucket_storage_path};
378+
const cli_options = { name: options.name, owner: options.owner || NC_CORETEST, path: bucket_storage_path};
379379
await exec_manage_cli(nc_nsfs_manage_entity_types.BUCKET, nc_nsfs_manage_actions.ADD, cli_options);
380380
}
381381

0 commit comments

Comments
 (0)