Skip to content

Commit c23af27

Browse files
authored
Merge pull request #154 from CyberSource/adding-pgp-encryption-for-batch-upload-api
Adding pgp encryption for batch upload api
2 parents 01ace59 + f82cc85 commit c23af27

File tree

12 files changed

+901
-1
lines changed

12 files changed

+901
-1
lines changed

CyberSource/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@
14821482

14831483
# import api into sdk package
14841484
from .api.o_auth_api import OAuthApi
1485+
from .api.batch_upload_with_mtls_api import BatchUploadWithMTLSApi
14851486
from .api.batches_api import BatchesApi
14861487
from .api.bin_lookup_api import BinLookupApi
14871488
from .api.chargeback_details_api import ChargebackDetailsApi

CyberSource/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@
6363
from .reversal_api import ReversalApi
6464
from .taxes_api import TaxesApi
6565
from .void_api import VoidApi
66+
from .batch_upload_with_mtls_api import BatchUploadWithMTLSApi
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

CyberSource/utilities/file_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from pathlib import Path
2+
from typing import List, Tuple
3+
4+
5+
def validate_path(paths: List[Tuple[str, str]]) -> None:
6+
"""
7+
Validate that all specified paths exist.
8+
9+
Args:
10+
paths: List of tuples containing (path, description)
11+
12+
Raises:
13+
ValueError: If a required path is None or empty
14+
FileNotFoundError: If any of the paths don't exist
15+
TypeError: If the paths parameter is not a list of tuples
16+
"""
17+
if not isinstance(paths, list):
18+
raise TypeError("paths must be a list of (path, description) tuples")
19+
20+
for path_tuple in paths:
21+
if not isinstance(path_tuple, tuple) or len(path_tuple) != 2:
22+
raise TypeError(
23+
"Each item in the paths list must be a (path, description) tuple"
24+
)
25+
26+
path, description = path_tuple
27+
28+
# Check if this is an optional parameter (currently only "Server trust certificate")
29+
is_optional = "Server trust certificate" in description
30+
31+
if path is None:
32+
if is_optional:
33+
continue # Skip validation for None paths that are optional
34+
else:
35+
raise ValueError(f"{description} is required but was None")
36+
37+
if not path:
38+
raise ValueError(f"{description} path is required")
39+
40+
if not Path(path).exists():
41+
raise FileNotFoundError(f"{description} not found: {path}")

CyberSource/utilities/pgpBatchUpload/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)