77from shutil import rmtree
88import multiprocessing as mp
99import traceback
10+ import fnmatch
1011import requests
1112
13+ import boto3
1214import botocore
1315from cumulus_logger import CumulusLogger
1416from cumulus_process import Process , s3
@@ -116,8 +118,36 @@ def download_file_from_s3(self, s3file, working_dir):
116118 str
117119 full path of the downloaded file
118120 """
121+ # Extract bucket name from S3 URI
122+ bucket_name = s3file .split ('/' )[2 ] # s3://bucket-name/path/to/file
123+
124+ # Check if we need to assume a role for this bucket
125+ role_arn = self ._get_role_for_bucket (bucket_name )
126+
119127 try :
128+ if role_arn :
129+ self .logger .info (f"Assuming role { role_arn } for download from bucket { bucket_name } " )
130+ credentials = self ._assume_role (role_arn )
131+
132+ # Create a new S3 client with assumed role credentials
133+ s3_client = boto3 .client (
134+ 's3' ,
135+ aws_access_key_id = credentials ['aws_access_key_id' ],
136+ aws_secret_access_key = credentials ['aws_secret_access_key' ],
137+ aws_session_token = credentials ['aws_session_token' ]
138+ )
139+
140+ # Extract key from S3 URI
141+ key = '/' .join (s3file .split ('/' )[3 :]) # path/to/file
142+ filename = os .path .basename (key )
143+ local_path = os .path .join (working_dir , filename )
144+
145+ # Download using the custom client
146+ s3_client .download_file (bucket_name , key , local_path )
147+ return local_path
148+
120149 return s3 .download (s3file , working_dir )
150+
121151 except botocore .exceptions .ClientError as ex :
122152 self .logger .error ("Error downloading file %s: %s" % (s3file , working_dir ), exc_info = True )
123153 raise ex
@@ -132,10 +162,41 @@ def upload_file_to_s3(self, filename, uri):
132162 uri: str
133163 s3 string of file location
134164 """
165+ # Extract bucket name from S3 URI
166+ bucket_name = uri .split ('/' )[2 ] # s3://bucket-name/path/to/file
167+
168+ # Check if we need to assume a role for this bucket
169+ role_arn = self ._get_role_for_bucket (bucket_name )
170+
135171 try :
172+ if role_arn :
173+ self .logger .info (f"Assuming role { role_arn } for upload to bucket { bucket_name } " )
174+ credentials = self ._assume_role (role_arn )
175+
176+ # Create a new S3 client with assumed role credentials
177+ s3_client = boto3 .client (
178+ 's3' ,
179+ aws_access_key_id = credentials ['aws_access_key_id' ],
180+ aws_secret_access_key = credentials ['aws_secret_access_key' ],
181+ aws_session_token = credentials ['aws_session_token' ]
182+ )
183+
184+ # Extract key from S3 URI
185+ key = '/' .join (uri .split ('/' )[3 :]) # path/to/file
186+
187+ # Upload using the custom client
188+ s3_client .upload_file (
189+ filename ,
190+ bucket_name ,
191+ key ,
192+ ExtraArgs = {"ACL" : "bucket-owner-full-control" }
193+ )
194+ return uri
195+
136196 return s3 .upload (filename , uri , extra = {"ACL" : "bucket-owner-full-control" })
197+
137198 except botocore .exceptions .ClientError as ex :
138- self .logger .error ("Error uploading file %s: %s" % (os .path .basename (os . path . basename ( filename ) ), str (ex )), exc_info = True )
199+ self .logger .error ("Error uploading file %s: %s" % (os .path .basename (filename ), str (ex )), exc_info = True )
139200 raise ex
140201
141202 @staticmethod
@@ -366,11 +427,147 @@ def _is_valid_input(self, file_):
366427 data_type = file_ ['type' ]
367428 return re .match (self .processing_regex , input_file ) or data_type == "data"
368429
430+ def _get_role_for_bucket (self , bucket_name ):
431+ """Get the appropriate role to assume for a given bucket.
432+
433+ The role mappings should be configured in self.config['role_mappings'] as:
434+ {
435+ "exact-bucket-name": "arn:aws:iam::123456789012:role/MyRole",
436+ "bucket-prefix-*": "arn:aws:iam::123456789012:role/PrefixRole",
437+ "*bucket-suffix": "arn:aws:iam::123456789012:role/SuffixRole",
438+ "bucket-*pattern*": "arn:aws:iam::123456789012:role/PatternRole",
439+ "regex-pattern": "arn:aws:iam::123456789012:role/RegexRole"
440+ }
441+
442+ Supports exact matches, wildcard patterns, and regular expression matching.
443+
444+ Parameters
445+ ----------
446+ bucket_name: str
447+ Name of the S3 bucket
448+
449+ Returns
450+ -------
451+ str or None
452+ Role ARN to assume, or None if no role is needed
453+ """
454+ role_mappings = self .config .get ('role_mappings' , {})
455+
456+ # Check for exact bucket match first (fastest)
457+ if bucket_name in role_mappings :
458+ return role_mappings [bucket_name ]
459+
460+ # Check pattern matches
461+ for pattern , role in role_mappings .items ():
462+ # Skip exact matches (already checked above)
463+ if pattern == bucket_name :
464+ continue
465+
466+ # Handle regex patterns
467+ if self ._is_regex_pattern (pattern ):
468+ try :
469+ if re .match (pattern , bucket_name ):
470+ return role
471+ except re .error as ex :
472+ self .logger .warning (f"Invalid regex pattern '{ pattern } ': { ex } " )
473+ continue
474+ # Handle simple wildcard patterns
475+ elif pattern .endswith ('*' ):
476+ prefix = pattern [:- 1 ]
477+ if bucket_name .startswith (prefix ):
478+ return role
479+ elif pattern .startswith ('*' ):
480+ suffix = pattern [1 :]
481+ if bucket_name .endswith (suffix ):
482+ return role
483+ elif '*' in pattern :
484+ # Handle complex wildcard patterns
485+ if fnmatch .fnmatch (bucket_name , pattern ):
486+ return role
487+
488+ return None
489+
490+ def _is_regex_pattern (self , pattern ):
491+ """Check if a pattern is a regex pattern for optimization.
492+
493+ Parameters
494+ ----------
495+ pattern: str
496+ Pattern to check
497+
498+ Returns
499+ -------
500+ bool
501+ True if pattern is a regex pattern
502+ """
503+ # Quick checks for common regex indicators
504+ return (pattern .startswith ('^' ) or
505+ pattern .endswith ('$' ) or
506+ '(' in pattern or
507+ '|' in pattern or
508+ '[' in pattern or
509+ '\\ ' in pattern )
510+
511+ def _assume_role (self , role_arn ):
512+ """Assume an IAM role and return credentials.
513+
514+ Parameters
515+ ----------
516+ role_arn: str
517+ ARN of the role to assume
518+
519+ Returns
520+ -------
521+ dict
522+ Credentials dictionary with access_key, secret_key, and token
523+ """
524+ try :
525+ sts_client = boto3 .client ('sts' )
526+ response = sts_client .assume_role (
527+ RoleArn = role_arn ,
528+ RoleSessionName = 'ImageGeneratorSession'
529+ )
530+
531+ credentials = response ['Credentials' ]
532+ return {
533+ 'aws_access_key_id' : credentials ['AccessKeyId' ],
534+ 'aws_secret_access_key' : credentials ['SecretAccessKey' ],
535+ 'aws_session_token' : credentials ['SessionToken' ]
536+ }
537+ except Exception as ex :
538+ self .logger .error (f"Error assuming role { role_arn } : { ex } " , exc_info = True )
539+ raise
540+
369541 def _download_file (self , file_ ):
370542 """Download the input file from S3."""
371543 input_file = f's3://{ file_ ["bucket" ]} /{ file_ ["key" ]} '
544+
545+ # Check if we need to assume a role for this bucket
546+ role_arn = self ._get_role_for_bucket (file_ ["bucket" ])
547+
372548 try :
549+ if role_arn :
550+ self .logger .info (f"Assuming role { role_arn } for bucket { file_ ['bucket' ]} " )
551+ credentials = self ._assume_role (role_arn )
552+
553+ # Create a new S3 client with assumed role credentials
554+ s3_client = boto3 .client (
555+ 's3' ,
556+ aws_access_key_id = credentials ['aws_access_key_id' ],
557+ aws_secret_access_key = credentials ['aws_secret_access_key' ],
558+ aws_session_token = credentials ['aws_session_token' ]
559+ )
560+
561+ # Download using the custom client
562+ bucket = file_ ["bucket" ]
563+ key = file_ ["key" ]
564+ local_path = os .path .join (self .path , os .path .basename (key ))
565+
566+ s3_client .download_file (bucket , key , local_path )
567+ return local_path
568+
373569 return s3 .download (input_file , path = self .path )
570+
374571 except botocore .exceptions .ClientError as ex :
375572 self .logger .error ("Error downloading file from S3: {}" .format (ex ), exc_info = True )
376573 raise
0 commit comments