protegrity-ai-developer-python 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appython/__init__.py +12 -0
- appython/protector.py +554 -0
- appython/service/auth_provider.py +273 -0
- appython/service/auth_token_provider.py +45 -0
- appython/service/config.py +209 -0
- appython/service/payload_builder.py +141 -0
- appython/service/request_handler.py +115 -0
- appython/service/response_handler.py +78 -0
- appython/stats/__init__.py +3 -0
- appython/stats/collector.py +90 -0
- appython/stats/writer.py +185 -0
- appython/utils/codec_helper.py +86 -0
- appython/utils/constants.py +246 -0
- appython/utils/exceptions.py +141 -0
- appython/utils/input_preprocessor.py +325 -0
- appython/utils/output_postprocessor.py +99 -0
- protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
- protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
- protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
- protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
- protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
- protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
- protegrity_developer_python/__init__.py +4 -0
- protegrity_developer_python/scan.py +37 -0
- protegrity_developer_python/securefind.py +83 -0
- protegrity_developer_python/utils/ccn_processing.py +59 -0
- protegrity_developer_python/utils/config.py +60 -0
- protegrity_developer_python/utils/constants.py +123 -0
- protegrity_developer_python/utils/discover.py +49 -0
- protegrity_developer_python/utils/logger.py +23 -0
- protegrity_developer_python/utils/pii_processing.py +291 -0
- protegrity_developer_python/utils/protector.py +23 -0
- protegrity_developer_python/utils/semantic_guardrails.py +240 -0
- protegrity_developer_python/utils/transform.py +66 -0
- pty_migrate/__init__.py +1 -0
- pty_migrate/check_cmd.py +871 -0
- pty_migrate/cli.py +93 -0
- pty_migrate/config.py +127 -0
- pty_migrate/create_policy_cmd.py +795 -0
- pty_migrate/payloads/__init__.py +51 -0
- pty_migrate/payloads/alphabets.json +42 -0
- pty_migrate/payloads/dataelements.json +342 -0
- pty_migrate/payloads/datastores.json +7 -0
- pty_migrate/payloads/deploy_policy_ta.json +1 -0
- pty_migrate/payloads/masks.json +18 -0
- pty_migrate/payloads/members.json +62 -0
- pty_migrate/payloads/policies.json +13 -0
- pty_migrate/payloads/roles.json +32 -0
- pty_migrate/payloads/rules.json +1639 -0
- pty_migrate/payloads/sources.json +10 -0
- pty_migrate/payloads/trusted_apps.json +8 -0
- pty_migrate/ppc_client.py +371 -0
- pty_migrate/stats_cmd.py +87 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pluggable authentication providers for the Protegrity SDK.
|
|
3
|
+
|
|
4
|
+
Each provider implements authenticate_request() to sign/authenticate outgoing
|
|
5
|
+
HTTP requests according to a specific auth mechanism.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import requests as http_requests
|
|
10
|
+
|
|
11
|
+
from appython.utils.exceptions import InitializationError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthProvider:
|
|
15
|
+
"""Base class for authentication providers."""
|
|
16
|
+
|
|
17
|
+
def authenticate_request(self, method, url, headers, body):
|
|
18
|
+
"""Sign/authenticate an outgoing request.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
method: HTTP method (e.g., "POST").
|
|
22
|
+
url: Full request URL.
|
|
23
|
+
headers: Existing headers dict (will be mutated).
|
|
24
|
+
body: Request body (dict).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
dict: Updated headers with auth credentials added.
|
|
28
|
+
"""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def initialize(self):
|
|
32
|
+
"""Perform any upfront auth (login, token fetch, etc.)."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def get_request_kwargs(self):
|
|
36
|
+
"""Return extra kwargs to pass to requests.post() (e.g., cert for mTLS).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
dict: Extra keyword arguments for requests.
|
|
40
|
+
"""
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CognitoAuthProvider(AuthProvider):
|
|
45
|
+
"""Authenticates via Cognito login (Developer Edition behavior).
|
|
46
|
+
|
|
47
|
+
Performs a login call to get a JWT token, then attaches
|
|
48
|
+
the JWT and API key to every request.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, config):
|
|
52
|
+
self._email = config.get("de_email")
|
|
53
|
+
self._password = config.get("de_password")
|
|
54
|
+
self._api_key = config.get("de_api_key")
|
|
55
|
+
self._protect_host = config.get("protect_host")
|
|
56
|
+
self._jwt_token = None
|
|
57
|
+
|
|
58
|
+
def initialize(self):
|
|
59
|
+
if not self._email or not self._password:
|
|
60
|
+
raise InitializationError(
|
|
61
|
+
err_msg="Authentication failed: Both DEV_EDITION_EMAIL and DEV_EDITION_PASSWORD must be provided."
|
|
62
|
+
)
|
|
63
|
+
if not self._api_key:
|
|
64
|
+
raise InitializationError(
|
|
65
|
+
err_msg="Authentication failed: DEV_EDITION_API_KEY must be provided."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
headers = {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"x-api-key": self._api_key,
|
|
71
|
+
}
|
|
72
|
+
login_url = f"{self._protect_host}/auth/login"
|
|
73
|
+
payload = {"email": self._email, "password": self._password}
|
|
74
|
+
# Cap the login call so a stalled DE auth endpoint does not hang init.
|
|
75
|
+
response = http_requests.post(login_url, json=payload, headers=headers, timeout=30)
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
raise InitializationError(
|
|
78
|
+
err_msg=f"{response.json().get('error', 'Could not authenticate user.')}"
|
|
79
|
+
)
|
|
80
|
+
self._jwt_token = response.json().get("jwt_token", None)
|
|
81
|
+
|
|
82
|
+
def authenticate_request(self, method, url, headers, body):
|
|
83
|
+
headers["x-api-key"] = self._api_key
|
|
84
|
+
headers["Authorization"] = self._jwt_token
|
|
85
|
+
return headers
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AWSIAMAuthProvider(AuthProvider):
|
|
89
|
+
"""Authenticates requests using AWS SigV4 signing.
|
|
90
|
+
|
|
91
|
+
Requires botocore to be installed: pip install appython[aws]
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, config):
|
|
95
|
+
self._protect_host = config.get("protect_host")
|
|
96
|
+
self._session = None
|
|
97
|
+
self._credentials = None
|
|
98
|
+
self._signer = None
|
|
99
|
+
|
|
100
|
+
def initialize(self):
|
|
101
|
+
try:
|
|
102
|
+
import botocore.session
|
|
103
|
+
import botocore.auth
|
|
104
|
+
import botocore.awsrequest
|
|
105
|
+
except ImportError:
|
|
106
|
+
raise InitializationError(
|
|
107
|
+
err_msg="aws_iam auth mode requires botocore. Install with: pip install appython[aws]"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
session = botocore.session.get_session()
|
|
111
|
+
self._credentials = session.get_credentials()
|
|
112
|
+
if not self._credentials:
|
|
113
|
+
raise InitializationError(
|
|
114
|
+
err_msg="AWS credentials not found. Configure via AWS_PROFILE, "
|
|
115
|
+
"AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or instance role."
|
|
116
|
+
)
|
|
117
|
+
self._credentials = self._credentials.get_frozen_credentials()
|
|
118
|
+
|
|
119
|
+
def authenticate_request(self, method, url, headers, body):
|
|
120
|
+
import botocore.auth
|
|
121
|
+
import botocore.awsrequest
|
|
122
|
+
import json
|
|
123
|
+
|
|
124
|
+
# Build an AWSRequest for signing
|
|
125
|
+
body_bytes = json.dumps(body).encode("utf-8") if isinstance(body, dict) else body
|
|
126
|
+
aws_request = botocore.awsrequest.AWSRequest(
|
|
127
|
+
method=method, url=url, headers=headers, data=body_bytes
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Sign with SigV4 for execute-api service
|
|
131
|
+
signer = botocore.auth.SigV4Auth(self._credentials, "execute-api", self._region)
|
|
132
|
+
signer.add_auth(aws_request)
|
|
133
|
+
|
|
134
|
+
# Copy signed headers back
|
|
135
|
+
headers.update(dict(aws_request.headers))
|
|
136
|
+
return headers
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def _region(self):
|
|
140
|
+
"""Extract region from the protect host URL or fall back to env/default."""
|
|
141
|
+
# Try to extract from URL like https://xxx.execute-api.us-east-1.amazonaws.com/...
|
|
142
|
+
host = self._protect_host or ""
|
|
143
|
+
parts = host.split(".")
|
|
144
|
+
for i, part in enumerate(parts):
|
|
145
|
+
if part == "execute-api" and i + 1 < len(parts):
|
|
146
|
+
return parts[i + 1]
|
|
147
|
+
# AWS_REGION is set by Lambda and honored by boto3/awscli;
|
|
148
|
+
# AWS_DEFAULT_REGION is the older variant. Accept either.
|
|
149
|
+
return os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION") or "us-east-1"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class BearerTokenAuthProvider(AuthProvider):
|
|
153
|
+
"""Authenticates with a Bearer token (static or fetched via OAuth2 client_credentials)."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, config):
|
|
156
|
+
self._static_token = config.get("static_token")
|
|
157
|
+
self._token_endpoint = config.get("token_endpoint")
|
|
158
|
+
self._client_id = config.get("client_id")
|
|
159
|
+
self._client_secret = config.get("client_secret")
|
|
160
|
+
self._access_token = None
|
|
161
|
+
|
|
162
|
+
def initialize(self):
|
|
163
|
+
if self._static_token:
|
|
164
|
+
self._access_token = self._static_token
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if not self._token_endpoint:
|
|
168
|
+
raise InitializationError(
|
|
169
|
+
err_msg="bearer_token mode requires PTY_STATIC_TOKEN or "
|
|
170
|
+
"PTY_TOKEN_ENDPOINT + PTY_CLIENT_ID + PTY_CLIENT_SECRET."
|
|
171
|
+
)
|
|
172
|
+
self._fetch_token()
|
|
173
|
+
|
|
174
|
+
def _fetch_token(self):
|
|
175
|
+
"""Fetch access token from OAuth2 token endpoint using client_credentials grant."""
|
|
176
|
+
data = {
|
|
177
|
+
"grant_type": "client_credentials",
|
|
178
|
+
"client_id": self._client_id,
|
|
179
|
+
"client_secret": self._client_secret,
|
|
180
|
+
}
|
|
181
|
+
response = http_requests.post(
|
|
182
|
+
self._token_endpoint,
|
|
183
|
+
data=data,
|
|
184
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
185
|
+
timeout=30,
|
|
186
|
+
)
|
|
187
|
+
if response.status_code != 200:
|
|
188
|
+
raise InitializationError(
|
|
189
|
+
err_msg=f"Failed to fetch OAuth2 token: {response.status_code} {response.text}"
|
|
190
|
+
)
|
|
191
|
+
self._access_token = response.json().get("access_token")
|
|
192
|
+
|
|
193
|
+
def authenticate_request(self, method, url, headers, body):
|
|
194
|
+
headers["Authorization"] = f"Bearer {self._access_token}"
|
|
195
|
+
return headers
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class NoneAuthProvider(AuthProvider):
|
|
199
|
+
"""No authentication — for internal/trusted networks."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, config):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def authenticate_request(self, method, url, headers, body):
|
|
205
|
+
return headers
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class MTLSAuthProvider(AuthProvider):
|
|
209
|
+
"""Mutual TLS authentication via client certificates.
|
|
210
|
+
|
|
211
|
+
Auth is at the TLS layer, not HTTP headers.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, config):
|
|
215
|
+
self._client_cert = config.get("client_cert")
|
|
216
|
+
self._client_key = config.get("client_key")
|
|
217
|
+
self._ca_cert = config.get("ca_cert")
|
|
218
|
+
|
|
219
|
+
def initialize(self):
|
|
220
|
+
if not self._client_cert or not self._client_key:
|
|
221
|
+
raise InitializationError(
|
|
222
|
+
err_msg="mtls mode requires PTY_CLIENT_CERT and PTY_CLIENT_KEY."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def authenticate_request(self, method, url, headers, body):
|
|
226
|
+
# mTLS auth is at TLS handshake layer — no header changes needed
|
|
227
|
+
return headers
|
|
228
|
+
|
|
229
|
+
def get_request_kwargs(self):
|
|
230
|
+
kwargs = {"cert": (self._client_cert, self._client_key)}
|
|
231
|
+
if self._ca_cert:
|
|
232
|
+
kwargs["verify"] = self._ca_cert
|
|
233
|
+
return kwargs
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Registry of auth mode string → provider class
|
|
237
|
+
_PROVIDER_REGISTRY = {
|
|
238
|
+
"cognito": CognitoAuthProvider,
|
|
239
|
+
"aws_iam": AWSIAMAuthProvider,
|
|
240
|
+
"bearer_token": BearerTokenAuthProvider,
|
|
241
|
+
"none": NoneAuthProvider,
|
|
242
|
+
"mtls": MTLSAuthProvider,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def create_auth_provider(config):
|
|
247
|
+
"""Factory: create and initialize the appropriate auth provider from config.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
config: Configuration dict from load_config().
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
AuthProvider: An initialized auth provider instance.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
InitializationError: If auth mode is missing or invalid.
|
|
257
|
+
"""
|
|
258
|
+
auth_mode = config.get("auth_mode")
|
|
259
|
+
if not auth_mode:
|
|
260
|
+
raise InitializationError(
|
|
261
|
+
err_msg="Cannot determine auth mode. Set PTY_AUTH_MODE or provide DEV_EDITION_* variables."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
provider_class = _PROVIDER_REGISTRY.get(auth_mode)
|
|
265
|
+
if not provider_class:
|
|
266
|
+
valid_modes = ", ".join(_PROVIDER_REGISTRY.keys())
|
|
267
|
+
raise InitializationError(
|
|
268
|
+
err_msg=f"Invalid PTY_AUTH_MODE='{auth_mode}'. Valid modes: {valid_modes}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
provider = provider_class(config)
|
|
272
|
+
provider.initialize()
|
|
273
|
+
return provider
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the AuthTokenProvider class, which is responsible for authenticating user credentials
|
|
3
|
+
and retrieving JWT tokens from the authentication endpoint.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import requests
|
|
8
|
+
from appython.utils.constants import HOST as host
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthTokenProvider:
|
|
12
|
+
def get_jwt_token(email: str, password: str,api_key:str):
|
|
13
|
+
"""
|
|
14
|
+
Authenticate user credentials and retrieve a JWT token.
|
|
15
|
+
|
|
16
|
+
This method sends a POST request to the authentication endpoint with the provided
|
|
17
|
+
email and password credentials. Upon successful authentication, it returns the
|
|
18
|
+
server response containing the JWT token and related authentication data.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
email (str): User's email address for authentication.
|
|
22
|
+
password (str): User's password for authentication.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
requests.Response: HTTP response object from the authentication request.
|
|
26
|
+
- On success (200): Contains JWT token in response body
|
|
27
|
+
- On failure (401, 403, etc.): Contains error details
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
requests.exceptions.RequestException: If the HTTP request fails due to network issues.
|
|
31
|
+
requests.exceptions.ConnectionError: If unable to connect to the authentication server.
|
|
32
|
+
requests.exceptions.Timeout: If the request times out.
|
|
33
|
+
"""
|
|
34
|
+
runtime_host = os.getenv('DEV_EDITION_HOST', host)
|
|
35
|
+
headers = {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"x-api-key": api_key,
|
|
38
|
+
}
|
|
39
|
+
base_url = f"https://{runtime_host}/auth/login"
|
|
40
|
+
payload = {"email": email, "password": password}
|
|
41
|
+
# Bound the login call so a hung auth endpoint does not stall SDK init.
|
|
42
|
+
# Retries are intentionally NOT applied here — auth errors are usually
|
|
43
|
+
# credential issues, not transient, and we do not want to amplify them.
|
|
44
|
+
response = requests.post(base_url, json=payload, headers=headers, timeout=30)
|
|
45
|
+
return response
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK configuration loader.
|
|
3
|
+
|
|
4
|
+
Resolution order (highest priority wins):
|
|
5
|
+
1. Environment variables (PTY_*, DEV_EDITION_*, AWS_*)
|
|
6
|
+
2. Config file (~/.protegrity/config.yaml or PTY_CONFIG_FILE path)
|
|
7
|
+
3. Built-in defaults
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import stat
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_DEFAULTS = {
|
|
16
|
+
"version": "1",
|
|
17
|
+
"request_timeout": "30",
|
|
18
|
+
"max_retries": "3",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Legacy DE host
|
|
22
|
+
_DE_HOST = "api.developer-edition.protegrity.com"
|
|
23
|
+
|
|
24
|
+
# Keys that hold secrets-at-rest. Dropped from file_config if the file is
|
|
25
|
+
# group/world readable (POSIX). Mirrors `~/.pgpass` behavior.
|
|
26
|
+
_SECRET_FILE_KEYS = ("static_token", "client_secret")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _file_is_secure(path):
|
|
30
|
+
"""True if `path` is readable only by the owner (mode & 077 == 0).
|
|
31
|
+
|
|
32
|
+
On Windows we can't check POSIX bits, so we trust the filesystem ACL.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
mode = os.stat(path).st_mode
|
|
36
|
+
except OSError:
|
|
37
|
+
return False
|
|
38
|
+
if os.name == "nt":
|
|
39
|
+
return True
|
|
40
|
+
return (mode & (stat.S_IRWXG | stat.S_IRWXO)) == 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_file_config():
|
|
44
|
+
"""Load optional YAML config file. Returns empty dict if not found or yaml unavailable.
|
|
45
|
+
|
|
46
|
+
Secret-bearing keys (see `_SECRET_FILE_KEYS`) are dropped with a stderr
|
|
47
|
+
warning if the file is group/world readable. Non-secret keys are still
|
|
48
|
+
returned so the rest of the SDK config keeps working.
|
|
49
|
+
"""
|
|
50
|
+
config_path = os.getenv(
|
|
51
|
+
"PTY_CONFIG_FILE", str(Path.home() / ".protegrity" / "config.yaml")
|
|
52
|
+
)
|
|
53
|
+
if not Path(config_path).is_file():
|
|
54
|
+
return {}
|
|
55
|
+
try:
|
|
56
|
+
import yaml
|
|
57
|
+
|
|
58
|
+
with open(config_path) as f:
|
|
59
|
+
cfg = yaml.safe_load(f) or {}
|
|
60
|
+
except ImportError:
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
if not _file_is_secure(config_path):
|
|
64
|
+
for k in _SECRET_FILE_KEYS:
|
|
65
|
+
if k in cfg:
|
|
66
|
+
print(
|
|
67
|
+
f" ⚠ Refusing to read '{k}' from {config_path}: file is "
|
|
68
|
+
f"group/world readable. Run: chmod 600 {config_path}",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
cfg.pop(k, None)
|
|
72
|
+
return cfg
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve(env_var, file_config, file_key, default=None):
|
|
76
|
+
"""Resolve a config value: env > file > default."""
|
|
77
|
+
return os.getenv(env_var) or file_config.get(file_key) or _DEFAULTS.get(file_key) or default
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _detect_auth_mode():
|
|
81
|
+
"""Auto-detect auth mode from environment when PTY_AUTH_MODE is not set."""
|
|
82
|
+
# If legacy DE vars are present, use cognito
|
|
83
|
+
if os.getenv("DEV_EDITION_EMAIL") and os.getenv("DEV_EDITION_PASSWORD"):
|
|
84
|
+
return "cognito"
|
|
85
|
+
|
|
86
|
+
# If PTY_CP_HOST is set, try to detect further
|
|
87
|
+
if os.getenv("PTY_CP_HOST"):
|
|
88
|
+
# Check for AWS credentials
|
|
89
|
+
if (
|
|
90
|
+
os.getenv("AWS_ACCESS_KEY_ID")
|
|
91
|
+
or os.getenv("AWS_PROFILE")
|
|
92
|
+
or os.getenv("AWS_SESSION_TOKEN")
|
|
93
|
+
):
|
|
94
|
+
return "aws_iam"
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _check_env_conflicts(file_config):
|
|
100
|
+
"""Raise early if environment variables indicate conflicting auth configurations."""
|
|
101
|
+
explicit_mode = _resolve("PTY_AUTH_MODE", file_config, "auth_mode")
|
|
102
|
+
has_de_vars = bool(os.getenv("DEV_EDITION_EMAIL") and os.getenv("DEV_EDITION_PASSWORD"))
|
|
103
|
+
has_aws_creds = bool(
|
|
104
|
+
os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_PROFILE")
|
|
105
|
+
)
|
|
106
|
+
has_te_host = bool(os.getenv("PTY_CP_HOST") or file_config.get("protect_host"))
|
|
107
|
+
|
|
108
|
+
# Conflict: PTY_AUTH_MODE=aws_iam but no PTY_CP_HOST
|
|
109
|
+
if explicit_mode == "aws_iam" and not has_te_host:
|
|
110
|
+
msg = (
|
|
111
|
+
"PTY_AUTH_MODE=aws_iam but PTY_CP_HOST is not set. "
|
|
112
|
+
"The SDK cannot use SigV4 without a Team Edition endpoint. "
|
|
113
|
+
)
|
|
114
|
+
if has_de_vars:
|
|
115
|
+
msg += (
|
|
116
|
+
"To use Developer Edition, unset: PTY_AUTH_MODE, AWS_ACCESS_KEY_ID, "
|
|
117
|
+
"AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_PROFILE"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
msg += "Set PTY_CP_HOST to your Team Edition endpoint."
|
|
121
|
+
raise EnvironmentError(msg)
|
|
122
|
+
|
|
123
|
+
# Warning: explicit aws_iam but DE vars also present (leftover from previous session)
|
|
124
|
+
if explicit_mode == "aws_iam" and has_te_host and has_de_vars:
|
|
125
|
+
import warnings
|
|
126
|
+
warnings.warn(
|
|
127
|
+
"PTY_AUTH_MODE=aws_iam but Developer Edition variables are also set "
|
|
128
|
+
"(DEV_EDITION_*). These will be ignored. "
|
|
129
|
+
"To silence this warning, unset: DEV_EDITION_EMAIL, "
|
|
130
|
+
"DEV_EDITION_PASSWORD, DEV_EDITION_API_KEY.",
|
|
131
|
+
stacklevel=2,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Warning: explicit cognito but TE vars also present (leftover from previous session)
|
|
135
|
+
if explicit_mode == "cognito" and has_aws_creds and has_te_host:
|
|
136
|
+
import warnings
|
|
137
|
+
warnings.warn(
|
|
138
|
+
"PTY_AUTH_MODE=cognito but Team Edition variables are also set "
|
|
139
|
+
"(PTY_CP_HOST, AWS credentials). These will be ignored. "
|
|
140
|
+
"To silence this warning, unset: PTY_CP_HOST, AWS_ACCESS_KEY_ID, "
|
|
141
|
+
"AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_PROFILE.",
|
|
142
|
+
stacklevel=2,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Conflict: both DE and TE credentials present without explicit mode
|
|
146
|
+
if not explicit_mode and has_de_vars and has_aws_creds and has_te_host:
|
|
147
|
+
raise EnvironmentError(
|
|
148
|
+
"Conflicting credentials: both DEV_EDITION_* and AWS/PTY_CP_HOST variables are set. "
|
|
149
|
+
"To use Team Edition, unset: DEV_EDITION_EMAIL, DEV_EDITION_PASSWORD, DEV_EDITION_API_KEY. "
|
|
150
|
+
"To use Developer Edition, unset: PTY_CP_HOST, AWS_ACCESS_KEY_ID, "
|
|
151
|
+
"AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_PROFILE."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def load_config():
|
|
156
|
+
"""Load SDK configuration with resolution order: env > file > defaults.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
dict: Resolved configuration dictionary.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
EnvironmentError: If conflicting credential variables are detected.
|
|
163
|
+
"""
|
|
164
|
+
file_config = _load_file_config()
|
|
165
|
+
|
|
166
|
+
# Detect conflicting environment variables early
|
|
167
|
+
_check_env_conflicts(file_config)
|
|
168
|
+
|
|
169
|
+
auth_mode = _resolve("PTY_AUTH_MODE", file_config, "auth_mode")
|
|
170
|
+
if not auth_mode:
|
|
171
|
+
auth_mode = _detect_auth_mode()
|
|
172
|
+
|
|
173
|
+
protect_host = _resolve("PTY_CP_HOST", file_config, "protect_host")
|
|
174
|
+
|
|
175
|
+
# Backward compat: if no PTY_CP_HOST but DE vars exist, build from DE host
|
|
176
|
+
if not protect_host and auth_mode == "cognito":
|
|
177
|
+
runtime_host = os.getenv("DEV_EDITION_HOST", _DE_HOST)
|
|
178
|
+
protect_host = f"https://{runtime_host}"
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
"protect_host": protect_host,
|
|
182
|
+
"auth_mode": auth_mode,
|
|
183
|
+
"version": _resolve("PTY_API_VERSION", file_config, "version", "1"),
|
|
184
|
+
"request_timeout": int(
|
|
185
|
+
_resolve("PTY_REQUEST_TIMEOUT", file_config, "request_timeout", "30")
|
|
186
|
+
),
|
|
187
|
+
"max_retries": int(
|
|
188
|
+
_resolve("PTY_MAX_RETRIES", file_config, "max_retries", "3")
|
|
189
|
+
),
|
|
190
|
+
# Bearer token mode
|
|
191
|
+
"token_endpoint": _resolve("PTY_TOKEN_ENDPOINT", file_config, "token_endpoint"),
|
|
192
|
+
"client_id": _resolve("PTY_CLIENT_ID", file_config, "client_id"),
|
|
193
|
+
"client_secret": _resolve("PTY_CLIENT_SECRET", file_config, "client_secret"),
|
|
194
|
+
"static_token": _resolve("PTY_STATIC_TOKEN", file_config, "static_token"),
|
|
195
|
+
# mTLS mode
|
|
196
|
+
"client_cert": _resolve("PTY_CLIENT_CERT", file_config, "client_cert"),
|
|
197
|
+
"client_key": _resolve("PTY_CLIENT_KEY", file_config, "client_key"),
|
|
198
|
+
"ca_cert": _resolve("PTY_CA_CERT", file_config, "ca_cert"),
|
|
199
|
+
# pty-migrate CLI settings (PPC admin endpoint; passwords stay env-only)
|
|
200
|
+
"ppc_host": _resolve("PTY_PPC_HOST", file_config, "ppc_host"),
|
|
201
|
+
"ppc_user": _resolve("PTY_PPC_USER", file_config, "ppc_user"),
|
|
202
|
+
"ppc_port": _resolve("PTY_PPC_PORT", file_config, "ppc_port"),
|
|
203
|
+
"workbench_user": _resolve("PTY_WORKBENCH_USER", file_config, "workbench_user"),
|
|
204
|
+
"stats_file": _resolve("PTY_STATS_FILE", file_config, "stats_file"),
|
|
205
|
+
# Legacy DE vars (for cognito provider)
|
|
206
|
+
"de_email": os.getenv("DEV_EDITION_EMAIL"),
|
|
207
|
+
"de_password": os.getenv("DEV_EDITION_PASSWORD"),
|
|
208
|
+
"de_api_key": os.getenv("DEV_EDITION_API_KEY"),
|
|
209
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the PayloadBuilder class, which constructs the payload for API requests
|
|
3
|
+
based on the operation type (PROTECT, UNPROTECT, REPROTECT).
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from appython.utils.constants import (
|
|
7
|
+
ErrorMessage,
|
|
8
|
+
OP_TYPE as op_type,
|
|
9
|
+
HOST as host,
|
|
10
|
+
VERSION as version,
|
|
11
|
+
LOG_RETURN_CODE_UNSUPPORTED as log_return_code
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def get_base_url(operation_type: str, config=None) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Constructs the base URL for the API endpoint based on the operation type.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
operation_type (str): The type of operation (e.g., 'PROTECT', 'UNPROTECT', 'REPROTECT').
|
|
20
|
+
config (dict, optional): SDK config dict. If provided, uses protect_host and version from it.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: The constructed base URL for the API call.
|
|
24
|
+
"""
|
|
25
|
+
if config and config.get("protect_host"):
|
|
26
|
+
# New config-driven path: PTY_CP_HOST already includes scheme + domain + base path
|
|
27
|
+
protect_host = config["protect_host"].rstrip("/")
|
|
28
|
+
api_version = config.get("version", "1")
|
|
29
|
+
return f"{protect_host}/v{api_version}/{operation_type}"
|
|
30
|
+
|
|
31
|
+
# Legacy path: build from DEV_EDITION_HOST
|
|
32
|
+
runtime_host = os.getenv('DEV_EDITION_HOST', host)
|
|
33
|
+
runtime_version = os.getenv('DEV_EDITION_VERSION', version)
|
|
34
|
+
|
|
35
|
+
return f"https://{runtime_host}/v{runtime_version}/{operation_type}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PayloadBuilder:
|
|
39
|
+
def build_api_request(input: dict, arguments: dict, operation_type: str, config=None):
|
|
40
|
+
"""
|
|
41
|
+
Builds the API request payload and return type metadata for a given data protection operation.
|
|
42
|
+
|
|
43
|
+
This method constructs a structured payload for the PROTECT, UNPROTECT, or REPROTECT operations,
|
|
44
|
+
including user info, data, data elements, encoding type, and optional IVs or tweaks. It also prepares
|
|
45
|
+
a return type template that describes how the response should be interpreted.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
input (dict): Contains the input data, its type, charset, and whether it's bulk.
|
|
49
|
+
arguments (dict): Contains operation parameters such as user, data element, response type, and optional IVs.
|
|
50
|
+
operation_type (str): The type of operation being performed ('PROTECT', 'UNPROTECT', or 'REPROTECT').
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
tuple: A tuple containing:
|
|
54
|
+
- payload_template (dict): The structured payload for the API request.
|
|
55
|
+
- return_type_template (dict): Metadata describing how to interpret the API response.
|
|
56
|
+
- url (str): The full API endpoint URL for the operation.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
Exception: If required parameters are missing, types are invalid, or unsupported operation types are used.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
payload_template = {
|
|
63
|
+
"user": None,
|
|
64
|
+
"encoding": "utf8",
|
|
65
|
+
"data_element": None,
|
|
66
|
+
"data": [],
|
|
67
|
+
"external_iv": None,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return_type_template = {
|
|
71
|
+
"is_bulk": None,
|
|
72
|
+
"response_type": None,
|
|
73
|
+
"type": None,
|
|
74
|
+
"charset": None,
|
|
75
|
+
"isENC": False,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
parameters = arguments["parameters"]
|
|
79
|
+
data_element = parameters["data_element"]
|
|
80
|
+
response_type = parameters["response_type"]
|
|
81
|
+
|
|
82
|
+
if "ENC" in data_element:
|
|
83
|
+
if op_type[operation_type] in ("PROTECT", "REPROTECT"):
|
|
84
|
+
if response_type != bytes:
|
|
85
|
+
raise Exception(f"26, {log_return_code[26]}")
|
|
86
|
+
else:
|
|
87
|
+
if input["input_datatype"] != bytes:
|
|
88
|
+
raise Exception(f"26, {log_return_code[26]}")
|
|
89
|
+
|
|
90
|
+
if "text" in data_element or "BYTE" in data_element:
|
|
91
|
+
return_type_template["isENC"] = True
|
|
92
|
+
payload_template["encoding"] = "base64"
|
|
93
|
+
|
|
94
|
+
if op_type[operation_type] in ("PROTECT", "UNPROTECT"):
|
|
95
|
+
payload_template["user"] = parameters["user"]
|
|
96
|
+
payload_template["data_element"] = data_element
|
|
97
|
+
|
|
98
|
+
if input["is_bulk"] is False:
|
|
99
|
+
payload_template["data"].append(input["data"])
|
|
100
|
+
elif input["is_bulk"] is True:
|
|
101
|
+
payload_template["data"] = input["data"]
|
|
102
|
+
else:
|
|
103
|
+
raise Exception(ErrorMessage.ERROR_SETTING_DATA.value)
|
|
104
|
+
|
|
105
|
+
return_type_template["type"] = input["type"]
|
|
106
|
+
|
|
107
|
+
if "external_iv" in parameters:
|
|
108
|
+
payload_template["external_iv"] = parameters["external_iv"]
|
|
109
|
+
|
|
110
|
+
elif op_type[operation_type] == "REPROTECT":
|
|
111
|
+
payload_template["user"] = parameters["user"]
|
|
112
|
+
payload_template["old_data_element"] = data_element
|
|
113
|
+
payload_template["data_element"] = parameters[
|
|
114
|
+
"new_data_element"
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if input["is_bulk"] is False:
|
|
118
|
+
payload_template["data"].append(input["data"])
|
|
119
|
+
elif input["is_bulk"] is True:
|
|
120
|
+
payload_template["data"] = input["data"]
|
|
121
|
+
else:
|
|
122
|
+
raise Exception(ErrorMessage.ERROR_SETTING_DATA.value)
|
|
123
|
+
|
|
124
|
+
return_type_template["type"] = input["type"]
|
|
125
|
+
|
|
126
|
+
if "old_external_iv_str" in parameters:
|
|
127
|
+
payload_template["old_external_iv"] = parameters[
|
|
128
|
+
"old_external_iv_str"
|
|
129
|
+
]
|
|
130
|
+
if "new_external_iv" in parameters:
|
|
131
|
+
payload_template["external_iv"] = parameters[
|
|
132
|
+
"new_external_iv"
|
|
133
|
+
]
|
|
134
|
+
else:
|
|
135
|
+
raise Exception(ErrorMessage.UNSUPPORTED_OPS_TYPE.value)
|
|
136
|
+
|
|
137
|
+
return_type_template["is_bulk"] = input["is_bulk"]
|
|
138
|
+
return_type_template["response_type"] = response_type
|
|
139
|
+
return_type_template["charset"] = input["charset"]
|
|
140
|
+
|
|
141
|
+
return payload_template, return_type_template, get_base_url(operation_type, config)
|