@@ -241,7 +241,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
241241 * permissions
242242 * a. First iteration - map_with_concurrency with concurrency rate of 10 entries at the same time.
243243 * a.1. if entry is dir file/entry is non json - we will return undefined
244- * a.2. if bucket is unaccessible by bucket policy - we will return undefined
244+ * a.2. if bucket is not owned by the account (or their root account for IAM users) - we will return undefined
245245 * a.3. if underlying storage of the bucket is unaccessible by the account's uid/gid - we will return undefined
246246 * a.4. else - return the bucket config info.
247247 * b. Second iteration - filter empty entries - filter will remove undefined values produced by the map_with_concurrency().
@@ -271,9 +271,9 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
271271 if ( err . rpc_code !== 'NO_SUCH_BUCKET' ) throw err ;
272272 }
273273 if ( ! bucket ) return ;
274- const bucket_policy_accessible = await this . has_bucket_action_permission ( bucket , account , 's3:ListBucket' ) ;
275- dbg . log2 ( `list_buckets: bucket_name ${ bucket_name } bucket_policy_accessible` , bucket_policy_accessible ) ;
276- if ( ! bucket_policy_accessible ) return ;
274+
275+ if ( ! this . has_bucket_ownership_permission ( bucket , account ) ) return ;
276+
277277 const fs_accessible = await this . validate_fs_bucket_access ( bucket , object_sdk ) ;
278278 if ( ! fs_accessible ) return ;
279279 return bucket ;
@@ -921,83 +921,6 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
921921 ///// UTILS /////
922922 /////////////////
923923
924- // TODO: move the following 3 functions - has_bucket_action_permission(), validate_fs_bucket_access(), _has_access_to_nsfs_dir()
925- // so they can be re-used
926- async has_bucket_action_permission ( bucket , account , action , bucket_path = "" ) {
927- dbg . log1 ( 'has_bucket_action_permission:' , bucket . name . unwrap ( ) , account . name . unwrap ( ) , account . _id , bucket . bucket_owner . unwrap ( ) ) ;
928-
929- const is_system_owner = Boolean ( bucket . system_owner ) && bucket . system_owner . unwrap ( ) === account . name . unwrap ( ) ;
930-
931- // If the system owner account wants to access the bucket, allow it
932- if ( is_system_owner ) return true ;
933- const is_owner = ( bucket . owner_account && bucket . owner_account . id === account . _id ) ;
934- const bucket_policy = bucket . s3_policy ;
935-
936- if ( ! bucket_policy ) {
937- // in case we do not have bucket policy
938- // we allow IAM account to access a bucket that that is owned by their root account
939- const is_iam_and_same_root_account_owner = account . owner !== undefined &&
940- account . owner === bucket . owner_account . id ;
941- return is_owner || is_iam_and_same_root_account_owner ;
942- }
943- if ( ! action ) {
944- throw new Error ( 'has_bucket_action_permission: action is required' ) ;
945- }
946-
947- let permission_by_name ;
948- const permission_by_id = await bucket_policy_utils . has_bucket_policy_permission (
949- bucket_policy ,
950- account . _id ,
951- action ,
952- `arn:aws:s3:::${ bucket . name . unwrap ( ) } ${ bucket_path } ` ,
953- undefined ,
954- { disallow_public_access : bucket . public_access_block ?. restrict_public_buckets }
955- ) ;
956- if ( permission_by_id === "DENY" ) return false ;
957- // we (currently) allow account identified to be both id and name,
958- // so if by-id failed, try also name
959- if ( account . owner === undefined ) {
960- permission_by_name = await bucket_policy_utils . has_bucket_policy_permission (
961- bucket_policy ,
962- account . name . unwrap ( ) ,
963- action ,
964- `arn:aws:s3:::${ bucket . name . unwrap ( ) } ${ bucket_path } ` ,
965- undefined ,
966- { disallow_public_access : bucket . public_access_block ?. restrict_public_buckets }
967- ) ;
968- }
969- if ( permission_by_name === 'DENY' ) return false ;
970- return is_owner || ( permission_by_id === 'ALLOW' || permission_by_name === 'ALLOW' ) ;
971- }
972-
973- async validate_fs_bucket_access ( bucket , object_sdk ) {
974- dbg . log1 ( 'bucketspace_fs.validate_fs_bucket_access:' , bucket . name . unwrap ( ) ) ;
975- const ns = bucket . namespace ;
976- const is_nsfs_bucket = object_sdk . is_nsfs_bucket ( ns ) ;
977- const accessible = is_nsfs_bucket ? await this . _has_access_to_nsfs_dir ( ns , object_sdk ) : false ;
978- dbg . log1 ( 'bucketspace_fs.validate_fs_bucket_access:' , bucket . name . unwrap ( ) , is_nsfs_bucket , accessible ) ;
979- return accessible ;
980- }
981-
982- async _has_access_to_nsfs_dir ( ns , object_sdk ) {
983- const account = object_sdk . requesting_account ;
984- dbg . log1 ( '_has_access_to_nsfs_dir: nsr: ' , ns , 'account.nsfs_account_config: ' , account && account . nsfs_account_config ) ;
985- // nsfs bucket
986- if ( ! account || ! account . nsfs_account_config || ( account . nsfs_account_config . uid === undefined ) ||
987- ( account . nsfs_account_config . gid === undefined ) ) return false ;
988- try {
989- dbg . log1 ( '_has_access_to_nsfs_dir: checking access:' , ns . write_resource , account . nsfs_account_config . uid , account . nsfs_account_config . gid ) ;
990- const path_to_check = path . join ( ns . write_resource . resource . fs_root_path , ns . write_resource . path || '' ) ;
991- const fs_context = this . prepare_fs_context ( object_sdk , ns . write_resource . resource . fs_backend ) ;
992- await nb_native ( ) . fs . checkAccess ( fs_context , path_to_check ) ;
993- return true ;
994- } catch ( err ) {
995- dbg . log0 ( '_has_access_to_nsfs_dir: failed' , err ) ;
996- if ( err . code === 'ENOENT' || err . code === 'EACCES' || ( err . code === 'EPERM' && err . message === 'Operation not permitted' ) ) return false ;
997- throw err ;
998- }
999- }
1000-
1001924 is_nsfs_containerized_user_anonymous ( token ) {
1002925 return ! token && ! process . env . NC_NSFS_NO_DB_ENV ;
1003926 }
@@ -1025,10 +948,10 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
1025948 /**
1026949 * _is_bucket_empty returns true if the given bucket is empty
1027950 *
1028- * @param {* } ns
1029- * @param {* } params
1030- * @param {string } name
1031- * @param {nb.ObjectSDK } object_sdk
951+ * @param {* } ns
952+ * @param {* } params
953+ * @param {string } name
954+ * @param {nb.ObjectSDK } object_sdk
1032955 *
1033956 * @returns {Promise<boolean> }
1034957 */
@@ -1055,8 +978,8 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
1055978 * _generate_reserved_tag_event_args returns the list of reserved
1056979 * bucket tags which have been modified and also returns a variable
1057980 * indicating if there has been any modifications at all.
1058- * @param {Record<string, string> } prev_tags_objectified
1059- * @param {Array<{key: string, value: string}> } new_tags
981+ * @param {Record<string, string> } prev_tags_objectified
982+ * @param {Array<{key: string, value: string}> } new_tags
1060983 */
1061984 static _generate_reserved_tag_event_args ( prev_tags_objectified , new_tags ) {
1062985 let reserved_tag_modified = false ;
@@ -1080,7 +1003,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
10801003 }
10811004
10821005 /**
1083- * @param {Array<{key: string, value: string}> } tagging
1006+ * @param {Array<{key: string, value: string}> } tagging
10841007 * @returns {Record<string, string> }
10851008 */
10861009 static _objectify_tagging_arr ( tagging ) {
@@ -1089,7 +1012,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
10891012
10901013 /**
10911014 * _create_uls creates underylying storage at the given storage_path
1092- * @param {* } fs_context - root fs_context
1015+ * @param {* } fs_context - root fs_context
10931016 * @param {* } acc_fs_context - fs_context associated with the performing account
10941017 * @param {* } name - bucket name
10951018 * @param {* } storage_path - bucket's storage path
@@ -1106,6 +1029,128 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
11061029 throw translate_error_codes ( error , entity_enum . BUCKET ) ;
11071030 }
11081031 }
1032+
1033+ //////////////////
1034+ // SHARED UTILS //
1035+ //////////////////
1036+
1037+ // TODO: move the below functions to a shared utils file so they can be re-used
1038+
1039+ /**
1040+ * is_bucket_owner checks if the account is the direct owner of the bucket
1041+ * @param {nb.Bucket } bucket
1042+ * @param {nb.Account } account
1043+ * @returns {boolean }
1044+ */
1045+ is_bucket_owner ( bucket , account ) {
1046+ if ( ! bucket ?. owner_account ?. id || ! account ?. _id ) return false ;
1047+ return bucket . owner_account . id === account . _id ;
1048+ }
1049+
1050+ /**
1051+ * is_iam_and_same_root_account_owner checks if the account is an IAM user and the bucket is owned by their root account
1052+ * @param {nb.Account } account
1053+ * @param {nb.Bucket } bucket
1054+ * @returns {boolean }
1055+ */
1056+ is_iam_and_same_root_account_owner ( account , bucket ) {
1057+ if ( ! account ?. owner || ! bucket ?. owner_account ?. id ) return false ;
1058+ return account . owner === bucket . owner_account . id ;
1059+ }
1060+
1061+ /**
1062+ * has_bucket_ownership_permission returns true if the account can list the bucket in ListBuckets operation
1063+ *
1064+ * aws-compliant behavior:
1065+ * - Root accounts can list buckets they own
1066+ * - IAM users can list their owner buckets
1067+ *
1068+ * @param {nb.Bucket } bucket
1069+ * @param {nb.Account } account
1070+ * @returns {boolean }
1071+ */
1072+ has_bucket_ownership_permission ( bucket , account ) {
1073+ // check direct ownership
1074+ if ( this . is_bucket_owner ( bucket , account ) ) return true ;
1075+
1076+ // special case: iam user can list the buckets of their owner
1077+ if ( this . is_iam_and_same_root_account_owner ( account , bucket ) ) return true ;
1078+
1079+ return false ;
1080+ }
1081+
1082+ async has_bucket_action_permission ( bucket , account , action , bucket_path = "" ) {
1083+ dbg . log1 ( 'has_bucket_action_permission:' , bucket . name . unwrap ( ) , account . name . unwrap ( ) , account . _id , bucket . bucket_owner . unwrap ( ) ) ;
1084+
1085+ // system_owner is deprecated since version 5.18, so we don't need to check it
1086+
1087+ // check ownership: direct owner or IAM user inheritance
1088+ const is_owner = this . is_bucket_owner ( bucket , account ) ;
1089+
1090+ const bucket_policy = bucket . s3_policy ;
1091+
1092+ if ( ! bucket_policy ) {
1093+ // in case we do not have bucket policy
1094+ // we allow IAM account to access a bucket that that is owned by their root account
1095+ return is_owner || this . is_iam_and_same_root_account_owner ( account , bucket ) ;
1096+ }
1097+ if ( ! action ) {
1098+ throw new Error ( 'has_bucket_action_permission: action is required' ) ;
1099+ }
1100+
1101+ let permission_by_name ;
1102+ const permission_by_id = await bucket_policy_utils . has_bucket_policy_permission (
1103+ bucket_policy ,
1104+ account . _id ,
1105+ action ,
1106+ `arn:aws:s3:::${ bucket . name . unwrap ( ) } ${ bucket_path } ` ,
1107+ undefined ,
1108+ { disallow_public_access : bucket . public_access_block ?. restrict_public_buckets }
1109+ ) ;
1110+ if ( permission_by_id === "DENY" ) return false ;
1111+ // we (currently) allow account identified to be both id and name,
1112+ // so if by-id failed, try also name
1113+ if ( account . owner === undefined ) {
1114+ permission_by_name = await bucket_policy_utils . has_bucket_policy_permission (
1115+ bucket_policy ,
1116+ account . name . unwrap ( ) ,
1117+ action ,
1118+ `arn:aws:s3:::${ bucket . name . unwrap ( ) } ${ bucket_path } ` ,
1119+ undefined ,
1120+ { disallow_public_access : bucket . public_access_block ?. restrict_public_buckets }
1121+ ) ;
1122+ }
1123+ if ( permission_by_name === 'DENY' ) return false ;
1124+ return is_owner || ( permission_by_id === 'ALLOW' || permission_by_name === 'ALLOW' ) ;
1125+ }
1126+
1127+ async validate_fs_bucket_access ( bucket , object_sdk ) {
1128+ dbg . log1 ( 'bucketspace_fs.validate_fs_bucket_access:' , bucket . name . unwrap ( ) ) ;
1129+ const ns = bucket . namespace ;
1130+ const is_nsfs_bucket = object_sdk . is_nsfs_bucket ( ns ) ;
1131+ const accessible = is_nsfs_bucket ? await this . _has_access_to_nsfs_dir ( ns , object_sdk ) : false ;
1132+ dbg . log1 ( 'bucketspace_fs.validate_fs_bucket_access:' , bucket . name . unwrap ( ) , is_nsfs_bucket , accessible ) ;
1133+ return accessible ;
1134+ }
1135+
1136+ async _has_access_to_nsfs_dir ( ns , object_sdk ) {
1137+ const account = object_sdk . requesting_account ;
1138+ dbg . log1 ( '_has_access_to_nsfs_dir: nsr: ' , ns , 'account.nsfs_account_config: ' , account && account . nsfs_account_config ) ;
1139+ // nsfs bucket
1140+ if ( ! account || ! account . nsfs_account_config || ( account . nsfs_account_config . uid === undefined ) ||
1141+ ( account . nsfs_account_config . gid === undefined ) ) return false ;
1142+ try {
1143+ dbg . log1 ( '_has_access_to_nsfs_dir: checking access:' , ns . write_resource , account . nsfs_account_config . uid , account . nsfs_account_config . gid ) ;
1144+ const path_to_check = path . join ( ns . write_resource . resource . fs_root_path , ns . write_resource . path || '' ) ;
1145+ const fs_context = this . prepare_fs_context ( object_sdk , ns . write_resource . resource . fs_backend ) ;
1146+ await nb_native ( ) . fs . checkAccess ( fs_context , path_to_check ) ;
1147+ return true ;
1148+ } catch ( err ) {
1149+ dbg . log0 ( '_has_access_to_nsfs_dir: failed' , err ) ;
1150+ if ( err . code === 'ENOENT' || err . code === 'EACCES' || ( err . code === 'EPERM' && err . message === 'Operation not permitted' ) ) return false ;
1151+ throw err ;
1152+ }
1153+ }
11091154}
11101155
11111156module . exports = BucketSpaceFS ;
0 commit comments