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,795 @@
|
|
|
1
|
+
"""pty-migrate create-policy — create DE policy on PPC (Team Edition).
|
|
2
|
+
|
|
3
|
+
Reads bundled DE policy payloads and creates resources on PPC.
|
|
4
|
+
By default, filters to only data elements and roles reported by usage stats.
|
|
5
|
+
If no stats exist or --full is specified, creates the complete DE policy.
|
|
6
|
+
|
|
7
|
+
Idempotent: queries PPC for existing resources and only creates the delta.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import getpass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from pty_migrate.payloads import load_all_payloads
|
|
17
|
+
from pty_migrate.ppc_client import PPCClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Resource creation order — dependencies first
|
|
21
|
+
RESOURCE_ORDER = [
|
|
22
|
+
"init",
|
|
23
|
+
"datastores",
|
|
24
|
+
"sources",
|
|
25
|
+
"roles",
|
|
26
|
+
"members",
|
|
27
|
+
"alphabets",
|
|
28
|
+
"masks",
|
|
29
|
+
"dataelements",
|
|
30
|
+
"applications",
|
|
31
|
+
"policies",
|
|
32
|
+
"rules",
|
|
33
|
+
"deploy",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_stats(stats_file=None):
|
|
38
|
+
"""Load usage stats from file. CLI flag > env > config file > default."""
|
|
39
|
+
from pty_migrate.config import resolve
|
|
40
|
+
resolved = resolve(stats_file, "PTY_STATS_FILE", "stats_file",
|
|
41
|
+
str(Path.home() / ".protegrity" / "usage_stats.json"))
|
|
42
|
+
path = Path(resolved)
|
|
43
|
+
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
return None, path
|
|
46
|
+
with open(path, "r") as f:
|
|
47
|
+
return json.load(f), path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_existing_names(client):
|
|
51
|
+
"""Query PPC for existing resource names (for delta computation)."""
|
|
52
|
+
return {
|
|
53
|
+
"datastores": {item.get("name", "").lower() for item in client.list_datastores()},
|
|
54
|
+
"sources": {item.get("name", "").lower() for item in client.list_sources()},
|
|
55
|
+
"roles": {item.get("name", "").lower() for item in client.list_roles()},
|
|
56
|
+
"alphabets": {item.get("label", item.get("name", "")).lower() for item in client.list_alphabets()},
|
|
57
|
+
"masks": {item.get("name", "").lower() for item in client.list_masks()},
|
|
58
|
+
"dataelements": {item.get("name", "").lower() for item in client.list_data_elements()},
|
|
59
|
+
"applications": {item.get("name", "").lower() for item in client.list_applications()},
|
|
60
|
+
"policies": {item.get("name", "").lower() for item in client.list_policies()},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_uid_maps(client, payloads):
|
|
65
|
+
"""Build mappings from payload positional index → actual PPC UID.
|
|
66
|
+
|
|
67
|
+
The payload files use sequential numeric strings ("1", "2", ...) as UIDs,
|
|
68
|
+
corresponding to the position of each resource in the payload array. On a
|
|
69
|
+
real PPC that already has resources, the actual assigned UIDs differ.
|
|
70
|
+
|
|
71
|
+
This queries the PPC for current resource UIDs by name and builds a mapping.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
dict with keys: roles, dataelements, sources, datastores, policies, applications
|
|
75
|
+
Each value is a dict mapping payload_index_str → actual_uid_str.
|
|
76
|
+
"""
|
|
77
|
+
def _name_to_uid(items, name_key="name"):
|
|
78
|
+
"""Build {lowercase_name: uid_str} from a PPC list response."""
|
|
79
|
+
result = {}
|
|
80
|
+
for item in items:
|
|
81
|
+
name = item.get(name_key, "").lower()
|
|
82
|
+
uid = str(item.get("uid", ""))
|
|
83
|
+
if name and uid:
|
|
84
|
+
result[name] = uid
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
ppc_roles = _name_to_uid(client.list_roles())
|
|
88
|
+
ppc_des = _name_to_uid(client.list_data_elements())
|
|
89
|
+
ppc_sources = _name_to_uid(client.list_sources())
|
|
90
|
+
ppc_datastores = _name_to_uid(client.list_datastores())
|
|
91
|
+
ppc_policies = _name_to_uid(client.list_policies())
|
|
92
|
+
ppc_apps = _name_to_uid(client.list_applications())
|
|
93
|
+
ppc_masks = _name_to_uid(client.list_masks())
|
|
94
|
+
|
|
95
|
+
# Map payload position (1-based) to actual UID by looking up the name
|
|
96
|
+
def _build_map(payload_list, ppc_lookup, name_key="name"):
|
|
97
|
+
mapping = {}
|
|
98
|
+
for i, item in enumerate(payload_list, 1):
|
|
99
|
+
name = item.get(name_key, "").lower()
|
|
100
|
+
actual_uid = ppc_lookup.get(name)
|
|
101
|
+
if actual_uid:
|
|
102
|
+
mapping[str(i)] = actual_uid
|
|
103
|
+
return mapping
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"roles": _build_map(payloads["roles"], ppc_roles),
|
|
107
|
+
"dataelements": _build_map(payloads["dataelements"], ppc_des),
|
|
108
|
+
"sources": _build_map(payloads["sources"], ppc_sources),
|
|
109
|
+
"datastores": _build_map(payloads["datastores"], ppc_datastores),
|
|
110
|
+
"policies": _build_map(payloads["policies"], ppc_policies),
|
|
111
|
+
"applications": _build_map(payloads["applications"], ppc_apps),
|
|
112
|
+
"masks": _build_map(payloads["masks"], ppc_masks),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _filter_payloads_by_stats(payloads, stats):
|
|
117
|
+
"""Filter payloads to only include resources reported in usage stats.
|
|
118
|
+
|
|
119
|
+
Filters data elements and roles. Infrastructure resources (datastores,
|
|
120
|
+
sources, alphabets, masks, applications, policies) are always included
|
|
121
|
+
as they are prerequisites.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
payloads: Full DE policy payloads dict.
|
|
125
|
+
stats: Usage stats dict with 'data_elements' and 'policy_users' keys.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Filtered payloads dict.
|
|
129
|
+
"""
|
|
130
|
+
stats_de_names = {name.lower() for name in stats.get("data_elements", {}).keys()}
|
|
131
|
+
stats_user_names = {name.lower() for name in stats.get("policy_users", {}).keys()}
|
|
132
|
+
|
|
133
|
+
filtered = dict(payloads) # Shallow copy
|
|
134
|
+
|
|
135
|
+
# Filter data elements to those in stats
|
|
136
|
+
if stats_de_names:
|
|
137
|
+
filtered["dataelements"] = [
|
|
138
|
+
de for de in payloads["dataelements"]
|
|
139
|
+
if de["name"].lower() in stats_de_names
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Build role index (1-based position in the full roles list)
|
|
143
|
+
all_roles = payloads["roles"]
|
|
144
|
+
role_name_to_index = {}
|
|
145
|
+
for i, role in enumerate(all_roles, 1):
|
|
146
|
+
role_name_to_index[role["name"].lower()] = str(i)
|
|
147
|
+
|
|
148
|
+
# Determine which roles contain the stats policy_users as members
|
|
149
|
+
# Members is a dict of "roles/{uid}/members" -> [member_payloads]
|
|
150
|
+
roles_containing_users = set() # role indices (str)
|
|
151
|
+
for endpoint, members in payloads["members"].items():
|
|
152
|
+
parts = endpoint.split("/")
|
|
153
|
+
if len(parts) >= 2:
|
|
154
|
+
role_uid = parts[1]
|
|
155
|
+
member_names = {m["name"].lower() for m in members}
|
|
156
|
+
if member_names & stats_user_names:
|
|
157
|
+
roles_containing_users.add(role_uid)
|
|
158
|
+
|
|
159
|
+
# Filter roles to those containing stats policy_users
|
|
160
|
+
if stats_user_names:
|
|
161
|
+
filtered["roles"] = [
|
|
162
|
+
role for role in all_roles
|
|
163
|
+
if role_name_to_index.get(role["name"].lower()) in roles_containing_users
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
# Filter members to only matching roles, only used members
|
|
167
|
+
filtered_role_names = {r["name"].lower() for r in filtered["roles"]}
|
|
168
|
+
filtered_members = {}
|
|
169
|
+
for endpoint, members in payloads["members"].items():
|
|
170
|
+
parts = endpoint.split("/")
|
|
171
|
+
if len(parts) >= 2:
|
|
172
|
+
role_uid = parts[1]
|
|
173
|
+
if role_uid in roles_containing_users:
|
|
174
|
+
# Only include members that are in stats policy_users
|
|
175
|
+
used_members = [
|
|
176
|
+
m for m in members
|
|
177
|
+
if m["name"].lower() in stats_user_names
|
|
178
|
+
]
|
|
179
|
+
if used_members:
|
|
180
|
+
filtered_members[endpoint] = used_members
|
|
181
|
+
filtered["members"] = filtered_members
|
|
182
|
+
|
|
183
|
+
# Filter rules to only matching role × data element combinations
|
|
184
|
+
filtered_role_uids = roles_containing_users
|
|
185
|
+
|
|
186
|
+
all_des = payloads["dataelements"]
|
|
187
|
+
de_name_to_index = {}
|
|
188
|
+
for i, de in enumerate(all_des, 1):
|
|
189
|
+
de_name_to_index[de["name"].lower()] = str(i)
|
|
190
|
+
|
|
191
|
+
filtered_de_uids = set()
|
|
192
|
+
for de_name in stats_de_names:
|
|
193
|
+
uid = de_name_to_index.get(de_name)
|
|
194
|
+
if uid:
|
|
195
|
+
filtered_de_uids.add(uid)
|
|
196
|
+
|
|
197
|
+
filtered["rules"] = [
|
|
198
|
+
rule for rule in payloads["rules"]
|
|
199
|
+
if str(rule.get("role")) in filtered_role_uids
|
|
200
|
+
and str(rule.get("dataElement")) in filtered_de_uids
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
return filtered
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _compute_delta(payloads, existing):
|
|
207
|
+
"""Remove resources that already exist on PPC.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
payloads: Filtered payloads to create.
|
|
211
|
+
existing: Dict of existing resource names on PPC (lowercase sets).
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Payloads with already-existing items removed.
|
|
215
|
+
"""
|
|
216
|
+
delta = dict(payloads)
|
|
217
|
+
|
|
218
|
+
delta["datastores"] = [
|
|
219
|
+
p for p in payloads.get("datastores", [])
|
|
220
|
+
if p.get("name", "").lower() not in existing.get("datastores", set())
|
|
221
|
+
]
|
|
222
|
+
delta["sources"] = [
|
|
223
|
+
p for p in payloads.get("sources", [])
|
|
224
|
+
if p.get("name", "").lower() not in existing.get("sources", set())
|
|
225
|
+
]
|
|
226
|
+
delta["roles"] = [
|
|
227
|
+
p for p in payloads.get("roles", [])
|
|
228
|
+
if p.get("name", "").lower() not in existing.get("roles", set())
|
|
229
|
+
]
|
|
230
|
+
delta["alphabets"] = [
|
|
231
|
+
p for p in payloads.get("alphabets", [])
|
|
232
|
+
if p.get("label", p.get("name", "")).lower() not in existing.get("alphabets", set())
|
|
233
|
+
]
|
|
234
|
+
delta["masks"] = [
|
|
235
|
+
p for p in payloads.get("masks", [])
|
|
236
|
+
if p.get("name", "").lower() not in existing.get("masks", set())
|
|
237
|
+
]
|
|
238
|
+
delta["dataelements"] = [
|
|
239
|
+
p for p in payloads.get("dataelements", [])
|
|
240
|
+
if p.get("name", "").lower() not in existing.get("dataelements", set())
|
|
241
|
+
]
|
|
242
|
+
delta["applications"] = [
|
|
243
|
+
p for p in payloads.get("applications", [])
|
|
244
|
+
if p.get("name", "").lower() not in existing.get("applications", set())
|
|
245
|
+
]
|
|
246
|
+
delta["policies"] = [
|
|
247
|
+
p for p in payloads.get("policies", [])
|
|
248
|
+
if p.get("name", "").lower() not in existing.get("policies", set())
|
|
249
|
+
]
|
|
250
|
+
# Rules and members are always attempted (PPC handles duplicates via 409)
|
|
251
|
+
delta["rules"] = payloads.get("rules", [])
|
|
252
|
+
delta["members"] = payloads.get("members", {})
|
|
253
|
+
delta["deploy"] = payloads.get("deploy", [])
|
|
254
|
+
|
|
255
|
+
return delta
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _print_plan(delta, dry_run=False):
|
|
259
|
+
"""Print what will be created."""
|
|
260
|
+
prefix = "[DRY RUN] " if dry_run else ""
|
|
261
|
+
print(f"\n{prefix}Resources to create:")
|
|
262
|
+
print(f" Datastores: {len(delta.get('datastores', []))}")
|
|
263
|
+
print(f" Sources: {len(delta.get('sources', []))}")
|
|
264
|
+
print(f" Roles: {len(delta.get('roles', []))}")
|
|
265
|
+
members_count = sum(len(v) for v in delta.get("members", {}).values())
|
|
266
|
+
print(f" Members: {members_count}")
|
|
267
|
+
print(f" Alphabets: {len(delta.get('alphabets', []))}")
|
|
268
|
+
print(f" Masks: {len(delta.get('masks', []))}")
|
|
269
|
+
print(f" Data elements: {len(delta.get('dataelements', []))}")
|
|
270
|
+
print(f" Applications: {len(delta.get('applications', []))}")
|
|
271
|
+
print(f" Policies: {len(delta.get('policies', []))}")
|
|
272
|
+
print(f" Rules: {len(delta.get('rules', []))}")
|
|
273
|
+
print()
|
|
274
|
+
|
|
275
|
+
if delta.get("dataelements"):
|
|
276
|
+
de_names = [de["name"] for de in delta["dataelements"]]
|
|
277
|
+
print(f" Data elements: {', '.join(de_names)}")
|
|
278
|
+
if delta.get("roles"):
|
|
279
|
+
role_names = [r["name"] for r in delta["roles"]]
|
|
280
|
+
print(f" Roles: {', '.join(role_names)}")
|
|
281
|
+
print()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def run_create_policy(args):
|
|
285
|
+
"""Execute the create-policy command."""
|
|
286
|
+
# Resolve PPC connection from CLI args + env vars + config file.
|
|
287
|
+
# Password precedence: env var > --ppc-password flag > interactive prompt.
|
|
288
|
+
# Env is preferred so secrets don't end up in shell history or `ps`.
|
|
289
|
+
from pty_migrate.config import resolve
|
|
290
|
+
args.ppc_host = resolve(args.ppc_host, "PTY_PPC_HOST", "ppc_host")
|
|
291
|
+
args.ppc_user = resolve(args.ppc_user, "PTY_PPC_USER", "ppc_user", "admin")
|
|
292
|
+
args.ppc_port = resolve(getattr(args, "ppc_port", None), "PTY_PPC_PORT", "ppc_port", 443)
|
|
293
|
+
try:
|
|
294
|
+
args.ppc_port = int(args.ppc_port)
|
|
295
|
+
except (TypeError, ValueError):
|
|
296
|
+
args.ppc_port = 443
|
|
297
|
+
|
|
298
|
+
# Password precedence: --ppc-password flag > PTY_PPC_PASSWORD env
|
|
299
|
+
# > ppc_password in ~/.protegrity/config.yaml (opt-in + chmod 600)
|
|
300
|
+
# > interactive prompt.
|
|
301
|
+
from pty_migrate.config import resolve_password
|
|
302
|
+
cli_password = args.ppc_password
|
|
303
|
+
resolved_pw, pw_source = resolve_password(
|
|
304
|
+
cli_password, "PTY_PPC_PASSWORD", "ppc_password"
|
|
305
|
+
)
|
|
306
|
+
if pw_source == "cli":
|
|
307
|
+
print(" ⚠ --ppc-password on the command line is recorded in shell history.")
|
|
308
|
+
print(" Prefer: export PTY_PPC_PASSWORD='...' (or omit to be prompted).")
|
|
309
|
+
args.ppc_password = resolved_pw
|
|
310
|
+
elif pw_source == "env":
|
|
311
|
+
args.ppc_password = resolved_pw
|
|
312
|
+
if cli_password and cli_password != resolved_pw:
|
|
313
|
+
print(" · Ignoring --ppc-password (PTY_PPC_PASSWORD env var takes precedence).")
|
|
314
|
+
elif pw_source == "file":
|
|
315
|
+
args.ppc_password = resolved_pw
|
|
316
|
+
print(" · PPC password loaded from ~/.protegrity/config.yaml (chmod 600 verified).")
|
|
317
|
+
elif args.ppc_host and sys.stdin.isatty():
|
|
318
|
+
try:
|
|
319
|
+
args.ppc_password = getpass.getpass(
|
|
320
|
+
f"PPC password for '{args.ppc_user}'@{args.ppc_host}: "
|
|
321
|
+
)
|
|
322
|
+
except (EOFError, KeyboardInterrupt):
|
|
323
|
+
print("\n ✗ Aborted.")
|
|
324
|
+
return 1
|
|
325
|
+
else:
|
|
326
|
+
args.ppc_password = None
|
|
327
|
+
|
|
328
|
+
# Workbench password: --flag > env > file (opt-in) > reuse admin password.
|
|
329
|
+
wb_cli = getattr(args, "workbench_password", None)
|
|
330
|
+
resolved_wb, wb_pw_source = resolve_password(
|
|
331
|
+
wb_cli, "PTY_WORKBENCH_PASSWORD", "workbench_password"
|
|
332
|
+
)
|
|
333
|
+
if wb_pw_source == "cli":
|
|
334
|
+
args.workbench_password = resolved_wb
|
|
335
|
+
wb_source = "--workbench-password flag"
|
|
336
|
+
elif wb_pw_source == "env":
|
|
337
|
+
args.workbench_password = resolved_wb
|
|
338
|
+
wb_source = "PTY_WORKBENCH_PASSWORD env var"
|
|
339
|
+
elif wb_pw_source == "file":
|
|
340
|
+
args.workbench_password = resolved_wb
|
|
341
|
+
wb_source = "~/.protegrity/config.yaml"
|
|
342
|
+
else:
|
|
343
|
+
args.workbench_password = args.ppc_password
|
|
344
|
+
wb_source = None # reused from admin — message printed only if we hit the workbench branch
|
|
345
|
+
|
|
346
|
+
args._workbench_password_explicit = wb_source is not None
|
|
347
|
+
args._workbench_password_source = wb_source
|
|
348
|
+
|
|
349
|
+
missing = [name for name, val in
|
|
350
|
+
(("--ppc-host / PTY_PPC_HOST", args.ppc_host),
|
|
351
|
+
("password (PTY_PPC_PASSWORD env, --ppc-password, or prompt)",
|
|
352
|
+
args.ppc_password))
|
|
353
|
+
if not val]
|
|
354
|
+
if missing:
|
|
355
|
+
print(f" ✗ Missing PPC credentials: {', '.join(missing)}")
|
|
356
|
+
print(" Set the environment variables or pass the matching CLI flags.")
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
# 1. Load bundled payloads (full DE policy definitions)
|
|
360
|
+
payloads = load_all_payloads()
|
|
361
|
+
|
|
362
|
+
# 2. Load stats (determines what to filter)
|
|
363
|
+
stats, stats_path = _load_stats(getattr(args, "stats_file", None))
|
|
364
|
+
full_mode = getattr(args, "full", False)
|
|
365
|
+
|
|
366
|
+
# Keep full payloads for UID mapping (rules reference positions in full list)
|
|
367
|
+
full_payloads = payloads
|
|
368
|
+
|
|
369
|
+
if stats and not full_mode:
|
|
370
|
+
print(f"Stats found at: {stats_path}")
|
|
371
|
+
de_names = list(stats.get("data_elements", {}).keys())
|
|
372
|
+
user_names = list(stats.get("policy_users", {}).keys())
|
|
373
|
+
print(f" Data elements in stats: {len(de_names)} — {', '.join(de_names)}")
|
|
374
|
+
print(f" Policy users in stats: {len(user_names)} — {', '.join(user_names)}")
|
|
375
|
+
print()
|
|
376
|
+
payloads = _filter_payloads_by_stats(payloads, stats)
|
|
377
|
+
elif full_mode:
|
|
378
|
+
print("Full mode: creating complete DE policy (ignoring stats filter)")
|
|
379
|
+
print()
|
|
380
|
+
else:
|
|
381
|
+
print(f"No stats found at: {stats_path}")
|
|
382
|
+
print("Creating complete DE policy (all data elements and roles)")
|
|
383
|
+
print()
|
|
384
|
+
|
|
385
|
+
# 3. Dry-run: show plan and exit
|
|
386
|
+
if getattr(args, "dry_run", False):
|
|
387
|
+
_print_plan(payloads, dry_run=True)
|
|
388
|
+
print("[DRY RUN] No changes made on PPC.")
|
|
389
|
+
return 0
|
|
390
|
+
|
|
391
|
+
# 4. Connect to PPC
|
|
392
|
+
user_was_explicit = getattr(args, "_ppc_user_explicit", False)
|
|
393
|
+
print(f"Connecting to PPC at {args.ppc_host}:{args.ppc_port}...")
|
|
394
|
+
client = PPCClient(args.ppc_host, args.ppc_user, args.ppc_password, args.ppc_port)
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
client.authenticate()
|
|
398
|
+
print(f" ✓ Authenticated as '{args.ppc_user}'")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
if "401" in str(e) and not user_was_explicit:
|
|
401
|
+
# Default user failed, try the alternate
|
|
402
|
+
alt_user = "workbench" if args.ppc_user == "admin" else "admin"
|
|
403
|
+
print(f" · User '{args.ppc_user}' failed, trying '{alt_user}'...")
|
|
404
|
+
client = PPCClient(args.ppc_host, alt_user, args.ppc_password, args.ppc_port)
|
|
405
|
+
try:
|
|
406
|
+
client.authenticate()
|
|
407
|
+
print(f" ✓ Authenticated as '{alt_user}'")
|
|
408
|
+
except Exception as e2:
|
|
409
|
+
print(f" ✗ Authentication failed for both '{args.ppc_user}' and '{alt_user}': {e2}")
|
|
410
|
+
return 1
|
|
411
|
+
else:
|
|
412
|
+
print(f" ✗ Authentication failed: {e}")
|
|
413
|
+
return 1
|
|
414
|
+
|
|
415
|
+
# 4b. Ensure PIM-capable user
|
|
416
|
+
# PIM APIs require 'workbench_management_policy_write' permission
|
|
417
|
+
# (via 'workbench_administrator' role). If the current user lacks PIM
|
|
418
|
+
# access, attempt to set up the 'workbench' user automatically.
|
|
419
|
+
pim_test = client._get("/v2/pim/datastores")
|
|
420
|
+
if pim_test.status_code == 403:
|
|
421
|
+
if user_was_explicit:
|
|
422
|
+
# User explicitly chose this user — don't second-guess
|
|
423
|
+
print(f"\n ✗ User '{client._user}' lacks PIM permissions (HTTP 403)")
|
|
424
|
+
print(f" The PIM API requires the 'workbench_administrator' role.")
|
|
425
|
+
print(f" Use --ppc-user with a PIM-capable user, or omit --ppc-user")
|
|
426
|
+
print(f" to let the script auto-create the 'workbench' user.")
|
|
427
|
+
return 1
|
|
428
|
+
|
|
429
|
+
print(f"\n · User '{client._user}' lacks PIM permissions, setting up 'workbench' user...")
|
|
430
|
+
wb_password = args.workbench_password
|
|
431
|
+
if args._workbench_password_explicit:
|
|
432
|
+
print(f" Using workbench password from {args._workbench_password_source}.")
|
|
433
|
+
else:
|
|
434
|
+
print(f" Using same password as '{args.ppc_user}' for workbench user")
|
|
435
|
+
print(f" (override with --workbench-password or PTY_WORKBENCH_PASSWORD).")
|
|
436
|
+
wb_exists = client.user_exists("workbench")
|
|
437
|
+
if not wb_exists:
|
|
438
|
+
created, resp = client.create_user(
|
|
439
|
+
"workbench", wb_password,
|
|
440
|
+
roles=["workbench_administrator"]
|
|
441
|
+
)
|
|
442
|
+
if created:
|
|
443
|
+
print(f" ✓ Created 'workbench' user with workbench_administrator role")
|
|
444
|
+
else:
|
|
445
|
+
print(f" ✗ Failed to create workbench user: {resp.status_code} {resp.text}")
|
|
446
|
+
return 1
|
|
447
|
+
else:
|
|
448
|
+
print(f" · 'workbench' user already exists")
|
|
449
|
+
|
|
450
|
+
# Ensure workbench_administrator role has all required PIM permissions
|
|
451
|
+
wb_permissions = [
|
|
452
|
+
"workbench_management_policy_write",
|
|
453
|
+
"workbench_management_policy_read",
|
|
454
|
+
"workbench_deployment_immutablepackage_export",
|
|
455
|
+
"workbench_deployment_certificate_export",
|
|
456
|
+
"cli_access",
|
|
457
|
+
"can_create_token",
|
|
458
|
+
]
|
|
459
|
+
if client.ensure_role_permissions("workbench_administrator", wb_permissions):
|
|
460
|
+
print(f" ✓ Role 'workbench_administrator' permissions confirmed")
|
|
461
|
+
else:
|
|
462
|
+
print(f" · Could not update role permissions (non-critical)")
|
|
463
|
+
|
|
464
|
+
# Re-authenticate as workbench
|
|
465
|
+
try:
|
|
466
|
+
client.re_authenticate("workbench", wb_password)
|
|
467
|
+
print(f" ✓ Re-authenticated as 'workbench'")
|
|
468
|
+
except Exception as e:
|
|
469
|
+
if wb_exists:
|
|
470
|
+
# workbench was pre-existing with a different password
|
|
471
|
+
print(f" ✗ Cannot authenticate as 'workbench' (password mismatch)")
|
|
472
|
+
if args._workbench_password_explicit:
|
|
473
|
+
print(f" The provided workbench password is wrong; pass the correct one.")
|
|
474
|
+
else:
|
|
475
|
+
print(f" The 'workbench' user pre-exists with a different password than '{args.ppc_user}'.")
|
|
476
|
+
print(f" Re-run with --workbench-password '<workbench-password>' or")
|
|
477
|
+
print(f" export PTY_WORKBENCH_PASSWORD='<workbench-password>'.")
|
|
478
|
+
else:
|
|
479
|
+
print(f" ✗ Failed to authenticate as newly created 'workbench': {e}")
|
|
480
|
+
return 1
|
|
481
|
+
|
|
482
|
+
# 5. Initialize PIM if needed
|
|
483
|
+
print("\nChecking PIM initialization...")
|
|
484
|
+
try:
|
|
485
|
+
client.init_pim()
|
|
486
|
+
print(" ✓ PIM ready")
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print(f" ✗ PIM initialization failed: {e}")
|
|
489
|
+
return 1
|
|
490
|
+
|
|
491
|
+
# 6. Query existing resources (for delta computation)
|
|
492
|
+
print("Querying existing resources on PPC...")
|
|
493
|
+
existing = _get_existing_names(client)
|
|
494
|
+
print(f" Found: {len(existing['dataelements'])} DEs, {len(existing['roles'])} roles, "
|
|
495
|
+
f"{len(existing['policies'])} policies")
|
|
496
|
+
|
|
497
|
+
# 7. Compute delta
|
|
498
|
+
delta = _compute_delta(payloads, existing)
|
|
499
|
+
_print_plan(delta)
|
|
500
|
+
|
|
501
|
+
total_new = (
|
|
502
|
+
len(delta["datastores"]) + len(delta["sources"]) + len(delta["roles"])
|
|
503
|
+
+ len(delta["alphabets"]) + len(delta["masks"]) + len(delta["dataelements"])
|
|
504
|
+
+ len(delta["applications"]) + len(delta["policies"])
|
|
505
|
+
)
|
|
506
|
+
if total_new == 0 and not delta["rules"] and not delta["members"]:
|
|
507
|
+
if not delta.get("deploy"):
|
|
508
|
+
print("Nothing new to create. PPC already has all required resources.")
|
|
509
|
+
return 0
|
|
510
|
+
print("All resources exist. Verifying deployment...")
|
|
511
|
+
print()
|
|
512
|
+
|
|
513
|
+
# 8. Create resources in dependency order
|
|
514
|
+
created = 0
|
|
515
|
+
skipped = 0
|
|
516
|
+
errors = 0
|
|
517
|
+
|
|
518
|
+
# Datastores
|
|
519
|
+
for payload in delta["datastores"]:
|
|
520
|
+
ok, _ = client.create_datastore(payload)
|
|
521
|
+
if ok:
|
|
522
|
+
created += 1
|
|
523
|
+
print(f" ✓ Created datastore: {payload.get('name')}")
|
|
524
|
+
else:
|
|
525
|
+
skipped += 1
|
|
526
|
+
|
|
527
|
+
# Sources
|
|
528
|
+
for payload in delta["sources"]:
|
|
529
|
+
ok, _ = client.create_source(payload)
|
|
530
|
+
if ok:
|
|
531
|
+
created += 1
|
|
532
|
+
print(f" ✓ Created source: {payload.get('name')}")
|
|
533
|
+
else:
|
|
534
|
+
skipped += 1
|
|
535
|
+
|
|
536
|
+
# Roles
|
|
537
|
+
for payload in delta["roles"]:
|
|
538
|
+
ok, _ = client.create_role(payload)
|
|
539
|
+
if ok:
|
|
540
|
+
created += 1
|
|
541
|
+
print(f" ✓ Created role: {payload.get('name')}")
|
|
542
|
+
else:
|
|
543
|
+
skipped += 1
|
|
544
|
+
|
|
545
|
+
# Alphabets
|
|
546
|
+
for payload in delta["alphabets"]:
|
|
547
|
+
ok, _ = client.create_alphabet(payload)
|
|
548
|
+
if ok:
|
|
549
|
+
created += 1
|
|
550
|
+
print(f" ✓ Created alphabet: {payload.get('label', payload.get('name'))}")
|
|
551
|
+
else:
|
|
552
|
+
skipped += 1
|
|
553
|
+
|
|
554
|
+
# Masks
|
|
555
|
+
for payload in delta["masks"]:
|
|
556
|
+
ok, resp = client.create_mask(payload)
|
|
557
|
+
if ok:
|
|
558
|
+
created += 1
|
|
559
|
+
print(f" ✓ Created mask: {payload.get('name')}")
|
|
560
|
+
else:
|
|
561
|
+
skipped += 1
|
|
562
|
+
|
|
563
|
+
# Build mask position→UID map so DEs that reference a mask via noEnc.maskUid
|
|
564
|
+
# (stored as positional index in our payload) can be remapped to the actual
|
|
565
|
+
# PPC-assigned UID before upload. Same shape as _build_uid_maps()["masks"]
|
|
566
|
+
# but needed earlier in the flow (before DE upload).
|
|
567
|
+
_ppc_masks_now = {
|
|
568
|
+
item.get("name", "").lower(): str(item.get("uid", ""))
|
|
569
|
+
for item in client.list_masks()
|
|
570
|
+
if item.get("name") and item.get("uid")
|
|
571
|
+
}
|
|
572
|
+
_mask_pos_to_uid = {}
|
|
573
|
+
for i, m in enumerate(full_payloads.get("masks", []), 1):
|
|
574
|
+
actual = _ppc_masks_now.get(m.get("name", "").lower())
|
|
575
|
+
if actual:
|
|
576
|
+
_mask_pos_to_uid[str(i)] = actual
|
|
577
|
+
|
|
578
|
+
# Same for alphabets — DEs reference them via unicodeGen2Token.alphabetUid
|
|
579
|
+
# as positional indexes into local alphabets.json.
|
|
580
|
+
_ppc_alphas_now = {
|
|
581
|
+
(item.get("label") or item.get("name") or "").lower(): str(item.get("uid", ""))
|
|
582
|
+
for item in client.list_alphabets()
|
|
583
|
+
if item.get("uid")
|
|
584
|
+
}
|
|
585
|
+
_alpha_pos_to_uid = {}
|
|
586
|
+
for i, a in enumerate(full_payloads.get("alphabets", []), 1):
|
|
587
|
+
key = (a.get("label") or a.get("name") or "").lower()
|
|
588
|
+
actual = _ppc_alphas_now.get(key)
|
|
589
|
+
if actual:
|
|
590
|
+
_alpha_pos_to_uid[str(i)] = actual
|
|
591
|
+
|
|
592
|
+
# Data elements
|
|
593
|
+
for payload in delta["dataelements"]:
|
|
594
|
+
remapped = payload
|
|
595
|
+
no_enc = payload.get("noEnc")
|
|
596
|
+
u2 = payload.get("unicodeGen2Token")
|
|
597
|
+
if isinstance(no_enc, dict) and "maskUid" in no_enc:
|
|
598
|
+
remapped = dict(payload)
|
|
599
|
+
remapped["noEnc"] = dict(no_enc)
|
|
600
|
+
remapped["noEnc"]["maskUid"] = _mask_pos_to_uid.get(
|
|
601
|
+
str(no_enc["maskUid"]), str(no_enc["maskUid"])
|
|
602
|
+
)
|
|
603
|
+
if isinstance(u2, dict) and "alphabetUid" in u2:
|
|
604
|
+
if remapped is payload:
|
|
605
|
+
remapped = dict(payload)
|
|
606
|
+
new_u2 = dict(u2)
|
|
607
|
+
new_u2["alphabetUid"] = _alpha_pos_to_uid.get(
|
|
608
|
+
str(u2["alphabetUid"]), str(u2["alphabetUid"])
|
|
609
|
+
)
|
|
610
|
+
remapped["unicodeGen2Token"] = new_u2
|
|
611
|
+
ok, resp = client.create_data_element(remapped)
|
|
612
|
+
if ok:
|
|
613
|
+
created += 1
|
|
614
|
+
print(f" ✓ Created data element: {payload.get('name')}")
|
|
615
|
+
else:
|
|
616
|
+
skipped += 1
|
|
617
|
+
|
|
618
|
+
# Applications
|
|
619
|
+
for payload in delta["applications"]:
|
|
620
|
+
ok, _ = client.create_application(payload)
|
|
621
|
+
if ok:
|
|
622
|
+
created += 1
|
|
623
|
+
print(f" ✓ Created application: {payload.get('name')}")
|
|
624
|
+
else:
|
|
625
|
+
skipped += 1
|
|
626
|
+
|
|
627
|
+
# Policies
|
|
628
|
+
for payload in delta["policies"]:
|
|
629
|
+
ok, _ = client.create_policy(payload)
|
|
630
|
+
if ok:
|
|
631
|
+
created += 1
|
|
632
|
+
print(f" ✓ Created policy: {payload.get('name')}")
|
|
633
|
+
else:
|
|
634
|
+
skipped += 1
|
|
635
|
+
|
|
636
|
+
# 9. Build UID mappings — payload positional index → actual PPC UID
|
|
637
|
+
# Must use full_payloads (not filtered) because rules reference positions
|
|
638
|
+
# in the original full payload arrays (e.g., dataElement "8" = city at
|
|
639
|
+
# position 8 in the full DE list).
|
|
640
|
+
print("\nResolving resource UIDs...")
|
|
641
|
+
uid_maps = _build_uid_maps(client, full_payloads)
|
|
642
|
+
|
|
643
|
+
# Members — uses remapped role/source UIDs
|
|
644
|
+
for endpoint, members in delta.get("members", {}).items():
|
|
645
|
+
# Remap role UID in endpoint: "roles/{payload_uid}/members" → actual UID
|
|
646
|
+
parts = endpoint.split("/")
|
|
647
|
+
if len(parts) >= 2 and parts[0] == "roles":
|
|
648
|
+
actual_role_uid = uid_maps["roles"].get(parts[1], parts[1])
|
|
649
|
+
endpoint = f"roles/{actual_role_uid}/members"
|
|
650
|
+
for member in members:
|
|
651
|
+
# Remap source UID in member payload
|
|
652
|
+
remapped_member = dict(member)
|
|
653
|
+
if "source" in remapped_member:
|
|
654
|
+
remapped_member["source"] = uid_maps["sources"].get(
|
|
655
|
+
remapped_member["source"], remapped_member["source"]
|
|
656
|
+
)
|
|
657
|
+
ok, _ = client.post_resource(endpoint, [remapped_member])
|
|
658
|
+
if ok:
|
|
659
|
+
created += 1
|
|
660
|
+
print(f" ✓ Added member: {member.get('name')} → {endpoint}")
|
|
661
|
+
else:
|
|
662
|
+
skipped += 1
|
|
663
|
+
|
|
664
|
+
# Rules — remap role/dataElement UIDs to actual PPC UIDs
|
|
665
|
+
policy_uid = uid_maps["policies"].get("1", "1") # Our policy is position 1
|
|
666
|
+
# DEs that use aes256CbcEnc don't support noAccessOperation=PROTECTED_VALUE.
|
|
667
|
+
# Strip that field from rules targeting them. Positions are 1-based to
|
|
668
|
+
# match the dataElement references in rules.json.
|
|
669
|
+
cbc_de_positions = {
|
|
670
|
+
str(i + 1)
|
|
671
|
+
for i, de in enumerate(full_payloads.get("dataelements", []))
|
|
672
|
+
if isinstance(de, dict) and "aes256CbcEnc" in de
|
|
673
|
+
}
|
|
674
|
+
rules_created = 0
|
|
675
|
+
rules_skipped = 0
|
|
676
|
+
rules_failed = 0
|
|
677
|
+
first_error = None
|
|
678
|
+
for rule in delta["rules"]:
|
|
679
|
+
remapped_rule = dict(rule)
|
|
680
|
+
if (
|
|
681
|
+
str(rule.get("dataElement")) in cbc_de_positions
|
|
682
|
+
and "noAccessOperation" in remapped_rule
|
|
683
|
+
):
|
|
684
|
+
remapped_rule.pop("noAccessOperation", None)
|
|
685
|
+
remapped_rule["role"] = uid_maps["roles"].get(
|
|
686
|
+
str(rule.get("role")), str(rule.get("role"))
|
|
687
|
+
)
|
|
688
|
+
remapped_rule["dataElement"] = uid_maps["dataelements"].get(
|
|
689
|
+
str(rule.get("dataElement")), str(rule.get("dataElement"))
|
|
690
|
+
)
|
|
691
|
+
if "mask" in remapped_rule:
|
|
692
|
+
remapped_rule["mask"] = uid_maps["masks"].get(
|
|
693
|
+
str(rule.get("mask")), str(rule.get("mask"))
|
|
694
|
+
)
|
|
695
|
+
ok, resp = client.create_rule(policy_uid, remapped_rule)
|
|
696
|
+
if ok:
|
|
697
|
+
rules_created += 1
|
|
698
|
+
elif resp and resp.status_code == 400:
|
|
699
|
+
rules_failed += 1
|
|
700
|
+
if first_error is None:
|
|
701
|
+
first_error = resp.text[:200]
|
|
702
|
+
else:
|
|
703
|
+
rules_skipped += 1
|
|
704
|
+
if rules_created:
|
|
705
|
+
print(f" ✓ Created {rules_created} rules")
|
|
706
|
+
if rules_skipped:
|
|
707
|
+
print(f" · {rules_skipped} rules already existed")
|
|
708
|
+
if rules_failed:
|
|
709
|
+
print(f" ✗ {rules_failed} rules failed (400 Bad Request)")
|
|
710
|
+
print(f" First error: {first_error}")
|
|
711
|
+
|
|
712
|
+
# Deploy — use actual datastore and policy UIDs
|
|
713
|
+
ds_uid = uid_maps["datastores"].get("1", "1") # Our datastore is position 1
|
|
714
|
+
app_uid = uid_maps["applications"].get("1") # None if app didn't resolve
|
|
715
|
+
if delta.get("deploy"):
|
|
716
|
+
# Only include the application in the deploy if we have a real PPC UID
|
|
717
|
+
# for it. If app creation failed (e.g. description validation) and PPC
|
|
718
|
+
# has no pre-existing app with the same name, skip it from the start to
|
|
719
|
+
# avoid a spurious "Application 'N' does not exist" error.
|
|
720
|
+
if app_uid:
|
|
721
|
+
deploy_payload = {"policies": [policy_uid], "applications": [app_uid]}
|
|
722
|
+
else:
|
|
723
|
+
deploy_payload = {"policies": [policy_uid], "applications": []}
|
|
724
|
+
ok, resp = client.deploy(ds_uid, deploy_payload)
|
|
725
|
+
if ok:
|
|
726
|
+
msg = f" ✓ Policy deployed to datastore (ds={ds_uid}, policy={policy_uid})"
|
|
727
|
+
print(msg)
|
|
728
|
+
if not app_uid:
|
|
729
|
+
print(" Note: application not included (creation failed or not found)")
|
|
730
|
+
else:
|
|
731
|
+
# Application might already be assigned to another datastore; retry without it
|
|
732
|
+
deploy_payload_no_app = {"policies": [policy_uid], "applications": []}
|
|
733
|
+
ok2, resp2 = client.deploy(ds_uid, deploy_payload_no_app)
|
|
734
|
+
if ok2:
|
|
735
|
+
print(f" ✓ Policy deployed to datastore (ds={ds_uid}, policy={policy_uid})")
|
|
736
|
+
print(f" Note: application not included (already assigned elsewhere)")
|
|
737
|
+
else:
|
|
738
|
+
# Use the most relevant response for error reporting
|
|
739
|
+
err_resp = resp2 if resp2 is not None else resp
|
|
740
|
+
status = getattr(err_resp, 'status_code', '?') if err_resp else '?'
|
|
741
|
+
body = ''
|
|
742
|
+
if err_resp is not None:
|
|
743
|
+
try:
|
|
744
|
+
body = err_resp.text[:200]
|
|
745
|
+
except Exception:
|
|
746
|
+
pass
|
|
747
|
+
print(f" ✗ Deployment failed (HTTP {status}): {body}")
|
|
748
|
+
errors += 1
|
|
749
|
+
|
|
750
|
+
# Summary
|
|
751
|
+
print(f"\n{'─' * 50}")
|
|
752
|
+
print(f"Summary: {created} created, {skipped} skipped (already existed)")
|
|
753
|
+
if errors:
|
|
754
|
+
print(f" {errors} errors")
|
|
755
|
+
return 1
|
|
756
|
+
print("Done.")
|
|
757
|
+
|
|
758
|
+
# Next steps: export key guidance
|
|
759
|
+
# Re-resolve the datastore UID from PPC so the printed curl reflects the
|
|
760
|
+
# actual id (e.g. after a reset the same logical datastore can land at id=2).
|
|
761
|
+
ds_uid_for_next_steps = ds_uid
|
|
762
|
+
target_ds_name = ""
|
|
763
|
+
if full_payloads.get("datastores"):
|
|
764
|
+
target_ds_name = full_payloads["datastores"][0].get("name", "").lower()
|
|
765
|
+
actual_datastores = client.list_datastores()
|
|
766
|
+
matched = next(
|
|
767
|
+
(d for d in actual_datastores
|
|
768
|
+
if d.get("name", "").lower() == target_ds_name and d.get("uid")),
|
|
769
|
+
None,
|
|
770
|
+
)
|
|
771
|
+
if matched:
|
|
772
|
+
ds_uid_for_next_steps = str(matched["uid"])
|
|
773
|
+
elif actual_datastores:
|
|
774
|
+
first_uid = actual_datastores[0].get("uid")
|
|
775
|
+
if first_uid:
|
|
776
|
+
ds_uid_for_next_steps = str(first_uid)
|
|
777
|
+
|
|
778
|
+
print(f"\n{'─' * 50}")
|
|
779
|
+
print(f"Next steps — Policy Agent setup:")
|
|
780
|
+
print(f"")
|
|
781
|
+
print(f" 1. Add the KMS export key to the datastore so the Policy Agent can")
|
|
782
|
+
print(f" export the encrypted policy package:")
|
|
783
|
+
print(f"")
|
|
784
|
+
print(f" curl -k -H \"Authorization: Bearer $TOKEN\" \\")
|
|
785
|
+
print(f" -H \"Content-Type: application/json\" \\")
|
|
786
|
+
print(f" -X POST https://{args.ppc_host}/pty/v2/pim/datastores/{ds_uid_for_next_steps}/export/keys \\")
|
|
787
|
+
print(f" -d '{{\"algorithm\":\"RSA-OAEP-256\",\"pem\":\"<KMS-PUBLIC-KEY-PEM>\"}}'")
|
|
788
|
+
print(f"")
|
|
789
|
+
print(f" The fingerprint returned must match PTY_DATASTORE_KEY on the Policy Agent Lambda.")
|
|
790
|
+
print(f"")
|
|
791
|
+
print(f" 2. Trigger the Policy Agent Lambda (or wait for the hourly CRON schedule).")
|
|
792
|
+
print(f"")
|
|
793
|
+
print(f" 3. Run: pty-migrate check")
|
|
794
|
+
print()
|
|
795
|
+
return 0
|