@@ -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
656704module . exports = BucketSpaceFS ;
0 commit comments