@@ -496,6 +496,10 @@ function _prepare_auth_request(req) {
496496 req . has_bucket_action_permission = async function ( bucket , action , bucket_path , req_query ) {
497497 return has_bucket_action_permission ( bucket , req . account , action , req_query , bucket_path ) ;
498498 } ;
499+
500+ req . has_list_bucket_permission = function ( bucket ) {
501+ return has_list_bucket_permission ( bucket , req . account , req . system ) ;
502+ } ;
499503}
500504
501505function _get_auth_info ( account , system , authorized_by , role , extra ) {
@@ -520,6 +524,100 @@ function _get_auth_info(account, system, authorized_by, role, extra) {
520524 return response ;
521525}
522526
527+ /**
528+ * is_system_owner checks if the account is the system owner
529+ * @param {Record<string, any> } system
530+ * @param {Record<string, any> } account
531+ * @returns {boolean }
532+ */
533+ function is_system_owner ( system , account ) {
534+ return system ?. owner ?. email ?. unwrap ( ) === account ?. email ?. unwrap ( ) ;
535+ }
536+
537+ /**
538+ * is_bucket_owner checks if the account is the direct owner of the bucket
539+ * @param {Record<string, any> } bucket
540+ * @param {Record<string, any> } account
541+ * @returns {boolean }
542+ */
543+ function is_bucket_owner ( bucket , account ) {
544+ return bucket ?. owner_account ?. email ?. unwrap ( ) === account ?. email ?. unwrap ( ) ;
545+ }
546+
547+ /**
548+ * has_iam_list_buckets_permission checks if the account has IAM policy granting s3:ListAllMyBuckets
549+ * for buckets owned by their root account
550+ * @param {Record<string, any> } bucket
551+ * @param {Record<string, any> } account
552+ * @returns {Promise<boolean> }
553+ */
554+ async function has_iam_list_buckets_permission ( bucket , account ) {
555+ if ( ! account ?. owner ) return false ;
556+
557+ const iam_policies = account . iam_user_policies || [ ] ;
558+ if ( iam_policies . length === 0 ) return false ;
559+
560+ // check each IAM policy for s3:ListAllMyBuckets permission
561+ for ( const iam_policy of iam_policies ) {
562+ const result = await s3_bucket_policy_utils . has_bucket_policy_permission (
563+ iam_policy . policy_document ,
564+ undefined ,
565+ 's3:ListAllMyBuckets' ,
566+ 'arn:aws:s3:::*' ,
567+ undefined ,
568+ { should_pass_principal : false }
569+ ) ;
570+
571+ if ( result === 'DENY' ) return false ;
572+
573+ if ( result === 'ALLOW' ) {
574+ const bucket_owner_id = bucket . owner_account ?. _id ;
575+ const account_owner_id = account . owner ?. _id ;
576+ const ownership_match = account_owner_id !== undefined && bucket_owner_id !== undefined &&
577+ String ( bucket_owner_id ) === String ( account_owner_id ) ;
578+
579+ // special case: check if root is OBC account and this is the claimed bucket
580+ const root_claimed_bucket = account . owner ?. bucket_claim_owner ?. name ?. unwrap ( ) ;
581+ const obc_claim_match = root_claimed_bucket && root_claimed_bucket === bucket . name ?. unwrap ( ) ;
582+
583+ if ( ownership_match || obc_claim_match ) return true ;
584+ }
585+ }
586+
587+ return false ;
588+ }
589+
590+ /**
591+ * has_list_bucket_permission returns true if the account can list the bucket in ListBuckets operation
592+ *
593+ * aws-compliant behavior:
594+ * - System owner can list all the buckets
595+ * - Root accounts can list buckets they own
596+ * - IAM users CANNOT inherit ListBuckets visibility from root
597+ * - IAM users need explicit IAM policy with s3:ListAllMyBuckets to list buckets
598+ *
599+ * @param {Record<string, any> } bucket
600+ * @param {Record<string, any> } account
601+ * @param {Record<string, any> } system
602+ * @returns {Promise<boolean> }
603+ */
604+ async function has_list_bucket_permission ( bucket , account , system ) {
605+ // system owner can list all the buckets
606+ if ( is_system_owner ( bucket . system , account ) ) return true ;
607+
608+ // check direct ownership
609+ if ( is_bucket_owner ( bucket , account ) ) return true ;
610+
611+ // special case: check bucket claim ownership (OBC)
612+ if ( account . bucket_claim_owner ?. name ?. unwrap ( ) === bucket . name . unwrap ( ) ) return true ;
613+
614+ // check IAM policy for s3:ListAllMyBuckets
615+ // Note: IAM users need this policy to list buckets owned by their root account
616+ if ( await has_iam_list_buckets_permission ( bucket , account ) ) return true ;
617+
618+ return false ;
619+ }
620+
523621/**
524622 * has_bucket_action_permission returns true if the requesting account has permission to perform
525623 * the given action on the given bucket.
@@ -538,14 +636,22 @@ function _get_auth_info(account, system, authorized_by, role, extra) {
538636 */
539637async function has_bucket_action_permission ( bucket , account , action , req_query , bucket_path = "" ) {
540638 dbg . log1 ( 'has_bucket_action_permission:' , bucket . name , account . email , bucket . owner_account . email ) ;
541- // If the system owner account wants to access the bucket, allow it
542- if ( bucket . system . owner . email . unwrap ( ) === account . email . unwrap ( ) ) return true ;
543639
544- const is_owner = ( bucket . owner_account . email . unwrap ( ) === account . email . unwrap ( ) ) ||
545- ( account . bucket_claim_owner && account . bucket_claim_owner . name . unwrap ( ) === bucket . name . unwrap ( ) ) ;
640+ // system owner can access all buckets
641+ if ( is_system_owner ( bucket . system , account ) ) return true ;
642+
643+ // check ownership: direct owner, IAM user inheritance, or OBC
644+ const is_owner = is_bucket_owner ( bucket , account ) ;
645+ const bucket_owner_id = bucket . owner_account ?. _id ;
646+ const account_owner_id = account . owner ?. _id ;
647+ const has_iam_access = account_owner_id !== undefined && bucket_owner_id !== undefined &&
648+ String ( bucket_owner_id ) === String ( account_owner_id ) ;
649+ const has_obc_access = account . bucket_claim_owner ?. name ?. unwrap ( ) === bucket . name . unwrap ( ) ;
650+ const owner = is_owner || has_iam_access || has_obc_access ;
651+
546652 const bucket_policy = bucket . s3_policy ;
547653
548- if ( ! bucket_policy ) return is_owner ;
654+ if ( ! bucket_policy ) return owner ;
549655 if ( ! action ) {
550656 throw new Error ( 'has_bucket_action_permission: action is required' ) ;
551657 }
@@ -560,7 +666,8 @@ async function has_bucket_action_permission(bucket, account, action, req_query,
560666 ) ;
561667
562668 if ( result === 'DENY' ) return false ;
563- return is_owner || result === 'ALLOW' ;
669+
670+ return owner || result === 'ALLOW' ;
564671}
565672
566673/**
0 commit comments