|
| 1 | +from pathlib import Path |
| 2 | +from typing import Optional |
| 3 | + |
| 4 | +import CyberSource.logging.log_factory as LogFactory |
| 5 | +from CyberSource.utilities.pgpBatchUpload.mutual_auth_upload import MutualAuthUpload |
| 6 | +from CyberSource.utilities.pgpBatchUpload.pgp_encryption import PgpEncryption |
| 7 | +from CyberSource.utilities.file_utils import validate_path |
| 8 | +from authenticationsdk.util.GlobalLabelParameters import GlobalLabelParameters |
| 9 | +from CyberSource.rest import ApiException |
| 10 | + |
| 11 | + |
| 12 | +class BatchUploadWithMTLSApi: |
| 13 | + """ |
| 14 | + A class for handling batch uploads to CyberSource using mTLS authentication. |
| 15 | +
|
| 16 | + This class provides methods for encrypting and uploading batch files to CyberSource |
| 17 | + using PGP encryption and mutual TLS authentication. It orchestrates the entire process |
| 18 | + from file validation, PGP encryption, to secure transmission via mTLS. |
| 19 | +
|
| 20 | + The class follows a secure workflow: |
| 21 | + 1. Validates input files and certificates |
| 22 | + 2. Encrypts the input file using PGP encryption |
| 23 | + 3. Establishes a secure mTLS connection with CyberSource |
| 24 | + 4. Uploads the encrypted file to CyberSource's batch processing endpoint |
| 25 | + 5. Handles responses and provides comprehensive error reporting |
| 26 | +
|
| 27 | + This class supports various authentication methods including separate key/cert files |
| 28 | + and can be used either directly or as a context manager with the 'with' statement. |
| 29 | +
|
| 30 | + Attributes: |
| 31 | + logger: Logger instance for logging operations and errors |
| 32 | + pgp_encryption: PgpEncryption instance for handling PGP encryption |
| 33 | + mutual_auth_upload: MutualAuthUpload instance for handling mTLS uploads |
| 34 | + """ |
| 35 | + |
| 36 | + _end_point = "/pts/v1/transaction-batch-upload" |
| 37 | + _max_size_bytes = 75 * 1024 * 1024 # 75 MB in bytes |
| 38 | + |
| 39 | + def __init__(self, log_config=None): |
| 40 | + """ |
| 41 | + Initialize the BatchUploadWithMTLSApi. |
| 42 | +
|
| 43 | + Args: |
| 44 | + log_config: Optional configuration for the logger. If provided, it should be |
| 45 | + a LogConfiguration instance with appropriate settings. |
| 46 | +
|
| 47 | + Example: |
| 48 | + ```python |
| 49 | + from CyberSource.logging.log_configuration import LogConfiguration |
| 50 | +
|
| 51 | + # Create and configure the logger |
| 52 | + log_config = LogConfiguration() |
| 53 | + log_config.set_enable_log(True) |
| 54 | + log_config.set_log_directory("logs") |
| 55 | + log_config.set_log_file_name("cybersource_batch.log") |
| 56 | + log_config.set_log_maximum_size(10 * 1024 * 1024) # 10 MB |
| 57 | + log_config.set_log_level("INFO") |
| 58 | + log_config.set_enable_masking(True) |
| 59 | + log_config.set_log_format("%(asctime)s - %(name)s - %(levelname)s - %(message)s") |
| 60 | + log_config.set_log_date_format("%Y-%m-%d %H:%M:%S") |
| 61 | +
|
| 62 | + # Create the batch upload API instance with logging |
| 63 | + batch_api = BatchUploadWithMTLSApi(log_config) |
| 64 | + ``` |
| 65 | + """ |
| 66 | + self.logger = LogFactory.setup_logger(self.__class__.__name__, log_config) |
| 67 | + self.pgp_encryption = PgpEncryption(log_config) |
| 68 | + self.mutual_auth_upload = MutualAuthUpload(log_config) |
| 69 | + |
| 70 | + def upload_batch_api_with_key_and_certs_file( |
| 71 | + self, |
| 72 | + input_file_path: str, |
| 73 | + environment_hostname: str, |
| 74 | + pgp_encryption_public_key_path: str, |
| 75 | + client_cert_path: str, |
| 76 | + client_key_path: str, |
| 77 | + server_trust_cert_path: str = None, |
| 78 | + client_key_password: Optional[str] = None, |
| 79 | + verify_ssl: bool = True, |
| 80 | + ) -> tuple: |
| 81 | + """ |
| 82 | + Upload a batch file using separate key and certificate files. |
| 83 | +
|
| 84 | + This method handles the complete process of batch file upload to CyberSource: |
| 85 | + 1. Validates all input parameters and files |
| 86 | + 2. Checks file size (must be under 75MB) and format (must be .csv) |
| 87 | + 3. Encrypts the input file using PGP with the provided public key |
| 88 | + 4. Uploads the encrypted file to CyberSource using mutual TLS authentication |
| 89 | + 5. Returns the server response or raises appropriate exceptions |
| 90 | +
|
| 91 | + The method supports secure communication through mutual TLS authentication, |
| 92 | + where both the client and server authenticate each other using certificates. |
| 93 | +
|
| 94 | + Args: |
| 95 | + input_file_path: Path to the CSV file to upload (must exist and be under 75MB) |
| 96 | + environment_hostname: CyberSource environment hostname (e.g., "secure-batch-test.cybersource.com") |
| 97 | + pgp_encryption_public_key_path: Path to the PGP public key file for encryption |
| 98 | + client_cert_path: Path to the client certificate file (.pem or .crt) |
| 99 | + client_key_path: Path to the client private key file (.key) |
| 100 | + server_trust_cert_path: Path to the server trust certificate file (optional) |
| 101 | + If not provided, system CA certificates will be used |
| 102 | + client_key_password: Optional password for the client private key if it's encrypted |
| 103 | + verify_ssl: Whether to verify SSL certificates (default: True) |
| 104 | + Set to False only for development/testing purposes. |
| 105 | + WARNING: Disabling SSL verification poses a significant security risk |
| 106 | + and should never be used in production environments as it makes the |
| 107 | + connection vulnerable to man-in-the-middle attacks. |
| 108 | +
|
| 109 | + Returns: |
| 110 | + tuple: A tuple containing (response_data, status_code, headers) where: |
| 111 | + - response_data: The response data from the server as a string, typically containing a batch ID and status information |
| 112 | + - status_code: The HTTP status code of the response |
| 113 | + - headers: The HTTP headers of the response |
| 114 | +
|
| 115 | + Raises: |
| 116 | + CyberSource.rest.ApiException: If there's an error during the upload process, including: |
| 117 | + - Validation errors (status 400): If required parameters are missing or invalid (e.g., file too large) |
| 118 | + - File not found errors (status 400): If any of the required files don't exist |
| 119 | + - API errors: If the response status is not successful (2xx) |
| 120 | + - Unexpected errors (status 500): For any other unexpected errors |
| 121 | +
|
| 122 | + Notes: |
| 123 | + This method will log warnings but not raise exceptions if: |
| 124 | + - The provided PGP key is not a public key |
| 125 | + - The PGP public key has expired |
| 126 | +
|
| 127 | + Example: |
| 128 | + ```python |
| 129 | + batch_api = BatchUploadWithMTLSApi() |
| 130 | + try: |
| 131 | + response = batch_api.upload_batch_api_with_key_and_certs_file( |
| 132 | + input_file_path="path/to/transactions.csv", |
| 133 | + environment_hostname="apitest.cybersource.com", |
| 134 | + pgp_encryption_public_key_path="path/to/cybersource_public.asc", |
| 135 | + client_cert_path="path/to/client.crt", |
| 136 | + client_key_path="path/to/client.key", |
| 137 | + server_trust_cert_path="path/to/server_ca.crt" |
| 138 | + ) |
| 139 | + print(f"Upload successful. Response: {response}") |
| 140 | + except ApiException as e: |
| 141 | + print(f"Upload failed: {str(e)}") |
| 142 | + print(f"Status code: {e.status}") |
| 143 | + print(f"Response body: {e.body}") |
| 144 | + ``` |
| 145 | + """ |
| 146 | + try: |
| 147 | + # Step 1: Create endpoint URL |
| 148 | + endpoint_url = self.get_base_url(environment_hostname) + self._end_point |
| 149 | + |
| 150 | + # Step 2: Validations |
| 151 | + validate_path( |
| 152 | + [ |
| 153 | + (input_file_path, "Input file"), |
| 154 | + (pgp_encryption_public_key_path, "PGP public key"), |
| 155 | + (client_cert_path, "Client certificate"), |
| 156 | + (client_key_path, "Client private key"), |
| 157 | + (server_trust_cert_path, "Server trust certificate"), |
| 158 | + ] |
| 159 | + ) |
| 160 | + |
| 161 | + # Validate file size (maximum 75 MB) |
| 162 | + file_size = Path(input_file_path).stat().st_size |
| 163 | + if file_size > self._max_size_bytes: |
| 164 | + error_msg = "Input file size exceeds the maximum allowed size of 75 MB" |
| 165 | + if self.logger is not None: |
| 166 | + self.logger.error(error_msg) |
| 167 | + raise ApiException( |
| 168 | + status=400, |
| 169 | + reason="Validation error: Input file size exceeds the maximum allowed size of 75 MB", |
| 170 | + ) |
| 171 | + |
| 172 | + # Validate file extension (.csv) |
| 173 | + file_extension = Path(input_file_path).suffix.lower() |
| 174 | + if file_extension != ".csv": |
| 175 | + error_msg = "Input file must have a .csv extension" |
| 176 | + if self.logger is not None: |
| 177 | + self.logger.error(error_msg) |
| 178 | + raise ApiException( |
| 179 | + status=400, |
| 180 | + reason="Validation error: Input file must have a .csv extension", |
| 181 | + ) |
| 182 | + |
| 183 | + # Step 3: PGP encryption |
| 184 | + encrypted_pgp_bytes = self.pgp_encryption.handle_encrypt_operation( |
| 185 | + input_file_path, pgp_encryption_public_key_path |
| 186 | + ) |
| 187 | + |
| 188 | + # Step 4: Upload the encrypted PGP file using mTLS |
| 189 | + # Replace the file extension with .pgp |
| 190 | + file_name = Path(input_file_path).stem + ".pgp" |
| 191 | + |
| 192 | + # Log a warning if SSL verification is disabled |
| 193 | + if not verify_ssl and self.logger is not None: |
| 194 | + self.logger.warning( |
| 195 | + "SSL verification is disabled. This should not be used in production environments." |
| 196 | + ) |
| 197 | + |
| 198 | + response_tuple = self.mutual_auth_upload.handle_upload_operation_using_private_key_and_certs( |
| 199 | + encrypted_pgp_bytes=encrypted_pgp_bytes, |
| 200 | + endpoint_url=endpoint_url, |
| 201 | + file_name=file_name, |
| 202 | + client_private_key_path=client_key_path, |
| 203 | + client_cert_path=client_cert_path, |
| 204 | + server_trust_cert_path=server_trust_cert_path, |
| 205 | + client_key_password=client_key_password, |
| 206 | + verify_ssl=verify_ssl, |
| 207 | + ) |
| 208 | + if self.logger is not None: |
| 209 | + self.logger.info("Batch file uploaded successfully") |
| 210 | + return response_tuple |
| 211 | + except ValueError: |
| 212 | + if self.logger is not None: |
| 213 | + self.logger.error( |
| 214 | + "Validation error: Input parameters failed validation requirements" |
| 215 | + ) |
| 216 | + raise ApiException( |
| 217 | + status=400, |
| 218 | + reason="Validation error: Input parameters failed validation requirements", |
| 219 | + ) |
| 220 | + except FileNotFoundError: |
| 221 | + if self.logger is not None: |
| 222 | + self.logger.error("File not found: Required file could not be located") |
| 223 | + raise ApiException( |
| 224 | + status=400, reason="File not found: Required file could not be located" |
| 225 | + ) |
| 226 | + except ApiException: |
| 227 | + if self.logger is not None: |
| 228 | + self.logger.error( |
| 229 | + "API error: Batch upload request to CyberSource Batch Upload API failed" |
| 230 | + ) |
| 231 | + raise |
| 232 | + except Exception: |
| 233 | + if self.logger is not None: |
| 234 | + self.logger.error( |
| 235 | + "Error in upload_batch_api_with_key_and_certs_file: Batch upload operation failed" |
| 236 | + ) |
| 237 | + raise ApiException( |
| 238 | + status=500, reason="Unexpected error during batch upload operation" |
| 239 | + ) |
| 240 | + |
| 241 | + @staticmethod |
| 242 | + def get_base_url(environment_hostname: str) -> str: |
| 243 | + """ |
| 244 | + Get the base URL from the environment hostname. |
| 245 | +
|
| 246 | + This method ensures that the environment hostname is properly formatted as a URL |
| 247 | + by adding the 'https://' prefix if not already present. It also validates that |
| 248 | + the hostname is not empty. |
| 249 | +
|
| 250 | + Args: |
| 251 | + environment_hostname: CyberSource environment hostname (e.g., "apitest.cybersource.com") |
| 252 | +
|
| 253 | + Returns: |
| 254 | + str: The base URL with https:// prefix (e.g., "https://apitest.cybersource.com") |
| 255 | +
|
| 256 | + Raises: |
| 257 | + CyberSource.rest.ApiException: If the environment hostname is None, empty, or consists only of whitespace |
| 258 | +
|
| 259 | + """ |
| 260 | + if not environment_hostname: |
| 261 | + raise ApiException( |
| 262 | + status=400, |
| 263 | + reason="Environment Host Name for Batch Upload API cannot be null or empty.", |
| 264 | + ) |
| 265 | + |
| 266 | + base_url = environment_hostname.strip() |
| 267 | + if not base_url.startswith(GlobalLabelParameters.HTTP_URL_PREFIX): |
| 268 | + base_url = GlobalLabelParameters.HTTP_URL_PREFIX + base_url |
| 269 | + return base_url |
0 commit comments