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.
Files changed (53) hide show
  1. appython/__init__.py +12 -0
  2. appython/protector.py +554 -0
  3. appython/service/auth_provider.py +273 -0
  4. appython/service/auth_token_provider.py +45 -0
  5. appython/service/config.py +209 -0
  6. appython/service/payload_builder.py +141 -0
  7. appython/service/request_handler.py +115 -0
  8. appython/service/response_handler.py +78 -0
  9. appython/stats/__init__.py +3 -0
  10. appython/stats/collector.py +90 -0
  11. appython/stats/writer.py +185 -0
  12. appython/utils/codec_helper.py +86 -0
  13. appython/utils/constants.py +246 -0
  14. appython/utils/exceptions.py +141 -0
  15. appython/utils/input_preprocessor.py +325 -0
  16. appython/utils/output_postprocessor.py +99 -0
  17. protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
  18. protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
  19. protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
  20. protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
  21. protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
  22. protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
  23. protegrity_developer_python/__init__.py +4 -0
  24. protegrity_developer_python/scan.py +37 -0
  25. protegrity_developer_python/securefind.py +83 -0
  26. protegrity_developer_python/utils/ccn_processing.py +59 -0
  27. protegrity_developer_python/utils/config.py +60 -0
  28. protegrity_developer_python/utils/constants.py +123 -0
  29. protegrity_developer_python/utils/discover.py +49 -0
  30. protegrity_developer_python/utils/logger.py +23 -0
  31. protegrity_developer_python/utils/pii_processing.py +291 -0
  32. protegrity_developer_python/utils/protector.py +23 -0
  33. protegrity_developer_python/utils/semantic_guardrails.py +240 -0
  34. protegrity_developer_python/utils/transform.py +66 -0
  35. pty_migrate/__init__.py +1 -0
  36. pty_migrate/check_cmd.py +871 -0
  37. pty_migrate/cli.py +93 -0
  38. pty_migrate/config.py +127 -0
  39. pty_migrate/create_policy_cmd.py +795 -0
  40. pty_migrate/payloads/__init__.py +51 -0
  41. pty_migrate/payloads/alphabets.json +42 -0
  42. pty_migrate/payloads/dataelements.json +342 -0
  43. pty_migrate/payloads/datastores.json +7 -0
  44. pty_migrate/payloads/deploy_policy_ta.json +1 -0
  45. pty_migrate/payloads/masks.json +18 -0
  46. pty_migrate/payloads/members.json +62 -0
  47. pty_migrate/payloads/policies.json +13 -0
  48. pty_migrate/payloads/roles.json +32 -0
  49. pty_migrate/payloads/rules.json +1639 -0
  50. pty_migrate/payloads/sources.json +10 -0
  51. pty_migrate/payloads/trusted_apps.json +8 -0
  52. pty_migrate/ppc_client.py +371 -0
  53. 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)