systemlink-cli 1.3.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.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
slcli/policy_click.py
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"""CLI commands for managing SystemLink auth policies and policy templates."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import questionary
|
|
8
|
+
|
|
9
|
+
from .cli_utils import validate_output_format
|
|
10
|
+
from .policy_utils import (
|
|
11
|
+
_build_policy_payload,
|
|
12
|
+
_display_policy_details,
|
|
13
|
+
_display_template_details,
|
|
14
|
+
_fetch_policy_details,
|
|
15
|
+
_fetch_template_details,
|
|
16
|
+
_format_policy_list_row,
|
|
17
|
+
_format_template_list_row,
|
|
18
|
+
_parse_properties_from_cli,
|
|
19
|
+
)
|
|
20
|
+
from .universal_handlers import FilteredResponse, UniversalResponseHandler
|
|
21
|
+
from .utils import (
|
|
22
|
+
ExitCodes,
|
|
23
|
+
format_success,
|
|
24
|
+
get_base_url,
|
|
25
|
+
handle_api_error,
|
|
26
|
+
make_api_request,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_policy_commands(cli: Any) -> None:
|
|
31
|
+
"""Register the 'policy' command group and its subcommands."""
|
|
32
|
+
|
|
33
|
+
@cli.group(name="auth")
|
|
34
|
+
def auth() -> None:
|
|
35
|
+
"""Manage SystemLink auth policies and policy templates."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@auth.group(name="policy")
|
|
39
|
+
def policy_group() -> None:
|
|
40
|
+
"""Manage authorization policies."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@auth.group(name="template")
|
|
44
|
+
def template_group() -> None:
|
|
45
|
+
"""Manage policy templates."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@policy_group.command(name="list")
|
|
49
|
+
@click.option(
|
|
50
|
+
"--type",
|
|
51
|
+
"policy_type",
|
|
52
|
+
type=click.Choice(["default", "internal", "custom", "role"], case_sensitive=False),
|
|
53
|
+
default=None,
|
|
54
|
+
help="Filter by policy type",
|
|
55
|
+
)
|
|
56
|
+
@click.option("--builtin", is_flag=True, help="Show built-in policies only")
|
|
57
|
+
@click.option("--name", type=str, default=None, help="Filter by name (contains search)")
|
|
58
|
+
@click.option(
|
|
59
|
+
"--sortby",
|
|
60
|
+
type=click.Choice(["name", "created", "updated"]),
|
|
61
|
+
default="name",
|
|
62
|
+
show_default=True,
|
|
63
|
+
help="Sort field",
|
|
64
|
+
)
|
|
65
|
+
@click.option(
|
|
66
|
+
"--order",
|
|
67
|
+
type=click.Choice(["asc", "desc"]),
|
|
68
|
+
default="asc",
|
|
69
|
+
show_default=True,
|
|
70
|
+
help="Sort order",
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--take",
|
|
74
|
+
"-t",
|
|
75
|
+
type=int,
|
|
76
|
+
default=None,
|
|
77
|
+
help="Limit results (default: 25 for table, all for JSON)",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--format",
|
|
81
|
+
"-f",
|
|
82
|
+
type=click.Choice(["table", "json"]),
|
|
83
|
+
default="table",
|
|
84
|
+
show_default=True,
|
|
85
|
+
help="Output format",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--skip",
|
|
89
|
+
"-s",
|
|
90
|
+
type=int,
|
|
91
|
+
default=0,
|
|
92
|
+
show_default=True,
|
|
93
|
+
help="Number of items to skip before returning results",
|
|
94
|
+
)
|
|
95
|
+
def list_policies(
|
|
96
|
+
policy_type: Optional[str],
|
|
97
|
+
builtin: bool,
|
|
98
|
+
name: Optional[str],
|
|
99
|
+
sortby: str,
|
|
100
|
+
order: str,
|
|
101
|
+
take: Optional[int],
|
|
102
|
+
format: str,
|
|
103
|
+
skip: int,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""List policies with optional filtering."""
|
|
106
|
+
validate_output_format(format)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from urllib.parse import urlencode
|
|
110
|
+
|
|
111
|
+
if take is None:
|
|
112
|
+
take = 25 if format == "table" else 100
|
|
113
|
+
|
|
114
|
+
base_url = f"{get_base_url()}/niauth/v1/policies"
|
|
115
|
+
|
|
116
|
+
def build_url(page_take: int, page_skip: int) -> str:
|
|
117
|
+
query_params: Dict[str, Any] = {
|
|
118
|
+
"take": page_take,
|
|
119
|
+
"skip": page_skip,
|
|
120
|
+
"sortby": sortby,
|
|
121
|
+
"order": "ascending" if order == "asc" else "descending",
|
|
122
|
+
}
|
|
123
|
+
if policy_type:
|
|
124
|
+
query_params["type"] = policy_type.lower()
|
|
125
|
+
if builtin:
|
|
126
|
+
query_params["builtIn"] = "true"
|
|
127
|
+
if name:
|
|
128
|
+
query_params["name"] = f"*{name}*"
|
|
129
|
+
return f"{base_url}?{urlencode(query_params)}"
|
|
130
|
+
|
|
131
|
+
if format == "json":
|
|
132
|
+
import json
|
|
133
|
+
|
|
134
|
+
all_policies: List[Dict[str, Any]] = []
|
|
135
|
+
current_skip = skip
|
|
136
|
+
remaining = take
|
|
137
|
+
|
|
138
|
+
while True:
|
|
139
|
+
page_take = min(remaining, 100) if remaining else 100
|
|
140
|
+
url = build_url(page_take, current_skip)
|
|
141
|
+
resp = make_api_request("GET", url, payload=None)
|
|
142
|
+
data = resp.json()
|
|
143
|
+
policies_page = data.get("policies", [])
|
|
144
|
+
|
|
145
|
+
if not policies_page:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
all_policies.extend(policies_page)
|
|
149
|
+
|
|
150
|
+
if remaining:
|
|
151
|
+
if len(all_policies) >= remaining:
|
|
152
|
+
all_policies = all_policies[:remaining]
|
|
153
|
+
break
|
|
154
|
+
remaining -= len(policies_page)
|
|
155
|
+
|
|
156
|
+
if len(policies_page) < page_take:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
current_skip += page_take
|
|
160
|
+
|
|
161
|
+
click.echo(json.dumps(all_policies, indent=2) if all_policies else "[]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
current_skip = skip
|
|
165
|
+
|
|
166
|
+
while True:
|
|
167
|
+
url = build_url(take, current_skip)
|
|
168
|
+
resp = make_api_request("GET", url, payload=None)
|
|
169
|
+
data = resp.json()
|
|
170
|
+
policies = data.get("policies", [])
|
|
171
|
+
total_count = data.get("totalCount")
|
|
172
|
+
|
|
173
|
+
if not policies and current_skip == skip:
|
|
174
|
+
click.echo("No policies found.")
|
|
175
|
+
return
|
|
176
|
+
if not policies:
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
combined_resp = FilteredResponse({"policies": policies})
|
|
180
|
+
UniversalResponseHandler.handle_list_response(
|
|
181
|
+
resp=combined_resp,
|
|
182
|
+
data_key="policies",
|
|
183
|
+
item_name="policy",
|
|
184
|
+
format_output=format,
|
|
185
|
+
formatter_func=_format_policy_list_row,
|
|
186
|
+
headers=["ID", "Name", "Type", "Built-in", "Statements"],
|
|
187
|
+
column_widths=[36, 30, 12, 10, 15],
|
|
188
|
+
enable_pagination=False,
|
|
189
|
+
page_size=take,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if total_count is not None:
|
|
193
|
+
shown = current_skip + len(policies)
|
|
194
|
+
remaining = max(total_count - shown, 0)
|
|
195
|
+
message = f"Showing {len(policies)} of {total_count} policy(ies)."
|
|
196
|
+
if remaining:
|
|
197
|
+
message += f" {remaining} more available."
|
|
198
|
+
click.echo(message)
|
|
199
|
+
|
|
200
|
+
current_skip += take
|
|
201
|
+
|
|
202
|
+
if len(policies) < take:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
is_tty = sys.stdout.isatty() and sys.stdin.isatty()
|
|
207
|
+
except Exception:
|
|
208
|
+
is_tty = False
|
|
209
|
+
|
|
210
|
+
if not is_tty:
|
|
211
|
+
break
|
|
212
|
+
if not questionary.confirm(f"Show next {take} policies?", default=True).ask():
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
handle_api_error(exc)
|
|
217
|
+
|
|
218
|
+
@policy_group.command(name="get")
|
|
219
|
+
@click.argument("policy_id")
|
|
220
|
+
@click.option(
|
|
221
|
+
"--format",
|
|
222
|
+
"-f",
|
|
223
|
+
type=click.Choice(["table", "json"]),
|
|
224
|
+
default="table",
|
|
225
|
+
show_default=True,
|
|
226
|
+
help="Output format",
|
|
227
|
+
)
|
|
228
|
+
def get_policy(policy_id: str, format: str) -> None:
|
|
229
|
+
"""Get policy details."""
|
|
230
|
+
validate_output_format(format)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
url = f"{get_base_url()}/niauth/v1/policies/{policy_id}"
|
|
234
|
+
resp = make_api_request("GET", url, payload=None)
|
|
235
|
+
_display_policy_details(resp.json(), format)
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
handle_api_error(exc)
|
|
238
|
+
|
|
239
|
+
@policy_group.command(name="create")
|
|
240
|
+
@click.argument("template_id")
|
|
241
|
+
@click.option("--name", required=True, help="Name for the new policy")
|
|
242
|
+
@click.option("--workspace", required=True, help="Target workspace ID")
|
|
243
|
+
@click.option(
|
|
244
|
+
"--properties",
|
|
245
|
+
"-p",
|
|
246
|
+
type=str,
|
|
247
|
+
multiple=True,
|
|
248
|
+
help="Custom properties as key=value (repeatable)",
|
|
249
|
+
)
|
|
250
|
+
def create_policy(template_id: str, name: str, workspace: str, properties: tuple) -> None:
|
|
251
|
+
"""Create a new policy from a template scoped to a workspace."""
|
|
252
|
+
from .utils import check_readonly_mode
|
|
253
|
+
|
|
254
|
+
check_readonly_mode("create a policy")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
properties_dict = _parse_properties_from_cli(properties) if properties else None
|
|
258
|
+
|
|
259
|
+
payload = _build_policy_payload(
|
|
260
|
+
name=name,
|
|
261
|
+
policy_type="custom",
|
|
262
|
+
statements=None,
|
|
263
|
+
template_id=template_id,
|
|
264
|
+
workspace=workspace,
|
|
265
|
+
properties=properties_dict,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
url = f"{get_base_url()}/niauth/v1/policies"
|
|
269
|
+
resp = make_api_request("POST", url, payload=payload)
|
|
270
|
+
created_policy = resp.json()
|
|
271
|
+
|
|
272
|
+
format_success(
|
|
273
|
+
"Policy created from template",
|
|
274
|
+
{
|
|
275
|
+
"name": created_policy.get("name"),
|
|
276
|
+
"id": created_policy.get("id"),
|
|
277
|
+
"type": created_policy.get("type"),
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
except ValueError as e:
|
|
281
|
+
click.echo(f"✗ Error: {str(e)}", err=True)
|
|
282
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
handle_api_error(exc)
|
|
285
|
+
|
|
286
|
+
@policy_group.command(name="update")
|
|
287
|
+
@click.argument("policy_id")
|
|
288
|
+
@click.option("--name", type=str, default=None, help="New policy name")
|
|
289
|
+
@click.option(
|
|
290
|
+
"--template",
|
|
291
|
+
"template_id",
|
|
292
|
+
type=str,
|
|
293
|
+
default=None,
|
|
294
|
+
help="Policy template ID to apply or reapply",
|
|
295
|
+
)
|
|
296
|
+
@click.option(
|
|
297
|
+
"--workspace",
|
|
298
|
+
type=str,
|
|
299
|
+
default=None,
|
|
300
|
+
help="Workspace ID (required when applying a template)",
|
|
301
|
+
)
|
|
302
|
+
@click.option(
|
|
303
|
+
"--properties",
|
|
304
|
+
"-p",
|
|
305
|
+
type=str,
|
|
306
|
+
multiple=True,
|
|
307
|
+
help="Updated custom properties as key=value (repeatable)",
|
|
308
|
+
)
|
|
309
|
+
def update_policy(
|
|
310
|
+
policy_id: str,
|
|
311
|
+
name: Optional[str],
|
|
312
|
+
template_id: Optional[str],
|
|
313
|
+
workspace: Optional[str],
|
|
314
|
+
properties: tuple,
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Update an existing policy, optionally applying a new template."""
|
|
317
|
+
from .utils import check_readonly_mode
|
|
318
|
+
|
|
319
|
+
check_readonly_mode("update a policy")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
url = f"{get_base_url()}/niauth/v1/policies/{policy_id}"
|
|
323
|
+
current_resp = make_api_request("GET", url, payload=None)
|
|
324
|
+
current_policy = current_resp.json()
|
|
325
|
+
|
|
326
|
+
properties_dict = (
|
|
327
|
+
_parse_properties_from_cli(properties)
|
|
328
|
+
if properties
|
|
329
|
+
else current_policy.get("properties")
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# If template is provided, use template; otherwise keep current statements
|
|
333
|
+
statements_list = None
|
|
334
|
+
if not template_id:
|
|
335
|
+
statements_list = current_policy.get("statements")
|
|
336
|
+
|
|
337
|
+
# Use provided workspace or current workspace
|
|
338
|
+
effective_workspace = workspace or current_policy.get("workspace")
|
|
339
|
+
|
|
340
|
+
payload = _build_policy_payload(
|
|
341
|
+
name=name or current_policy.get("name"),
|
|
342
|
+
policy_type=current_policy.get("type"),
|
|
343
|
+
statements=statements_list,
|
|
344
|
+
template_id=template_id or current_policy.get("templateId"),
|
|
345
|
+
workspace=effective_workspace,
|
|
346
|
+
properties=properties_dict,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
resp = make_api_request("PUT", url, payload=payload)
|
|
350
|
+
updated_policy = resp.json()
|
|
351
|
+
|
|
352
|
+
format_success(
|
|
353
|
+
"Policy updated",
|
|
354
|
+
{
|
|
355
|
+
"name": updated_policy.get("name"),
|
|
356
|
+
"id": updated_policy.get("id"),
|
|
357
|
+
"type": updated_policy.get("type"),
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
except ValueError as e:
|
|
361
|
+
click.echo(f"✗ Error: {str(e)}", err=True)
|
|
362
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
363
|
+
except Exception as exc:
|
|
364
|
+
handle_api_error(exc)
|
|
365
|
+
|
|
366
|
+
@policy_group.command(name="delete")
|
|
367
|
+
@click.argument("policy_id")
|
|
368
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
369
|
+
def delete_policy(policy_id: str, force: bool) -> None:
|
|
370
|
+
"""Delete a policy."""
|
|
371
|
+
from .utils import check_readonly_mode
|
|
372
|
+
|
|
373
|
+
check_readonly_mode("delete a policy")
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
if not force:
|
|
377
|
+
details = _fetch_policy_details(policy_id, handle_errors=False)
|
|
378
|
+
policy_name = details.get("name") if details else policy_id
|
|
379
|
+
if not questionary.confirm(
|
|
380
|
+
f"Delete policy '{policy_name}'?",
|
|
381
|
+
default=False,
|
|
382
|
+
).ask():
|
|
383
|
+
click.echo("Deletion cancelled.")
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
url = f"{get_base_url()}/niauth/v1/policies/{policy_id}"
|
|
387
|
+
resp = make_api_request("DELETE", url, payload=None)
|
|
388
|
+
if resp.status_code not in (200, 204):
|
|
389
|
+
resp.raise_for_status()
|
|
390
|
+
|
|
391
|
+
format_success("Policy deleted", {"id": policy_id})
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
handle_api_error(exc)
|
|
394
|
+
|
|
395
|
+
@policy_group.command(name="diff")
|
|
396
|
+
@click.argument("policy_id_1")
|
|
397
|
+
@click.argument("policy_id_2")
|
|
398
|
+
def diff_policies(policy_id_1: str, policy_id_2: str) -> None:
|
|
399
|
+
"""Show a basic diff between two policies."""
|
|
400
|
+
try:
|
|
401
|
+
url1 = f"{get_base_url()}/niauth/v1/policies/{policy_id_1}"
|
|
402
|
+
url2 = f"{get_base_url()}/niauth/v1/policies/{policy_id_2}"
|
|
403
|
+
resp1 = make_api_request("GET", url1, payload=None)
|
|
404
|
+
resp2 = make_api_request("GET", url2, payload=None)
|
|
405
|
+
p1 = resp1.json()
|
|
406
|
+
p2 = resp2.json()
|
|
407
|
+
|
|
408
|
+
def set_from_list(lst: Any) -> set:
|
|
409
|
+
return set(lst or [])
|
|
410
|
+
|
|
411
|
+
click.echo("\nPolicy Diff")
|
|
412
|
+
click.echo("-" * 80)
|
|
413
|
+
click.echo(f"Name: {p1.get('name','')} vs {p2.get('name','')}")
|
|
414
|
+
click.echo(f"Type: {p1.get('type','')} vs {p2.get('type','')}")
|
|
415
|
+
click.echo(f"Built-in: {p1.get('builtIn',False)} vs {p2.get('builtIn',False)}")
|
|
416
|
+
click.echo(f"Workspace: {p1.get('workspace','N/A')} vs {p2.get('workspace','N/A')}")
|
|
417
|
+
|
|
418
|
+
s1 = p1.get("statements", [])
|
|
419
|
+
s2 = p2.get("statements", [])
|
|
420
|
+
click.echo(f"\nStatements: {len(s1)} vs {len(s2)}")
|
|
421
|
+
|
|
422
|
+
# Aggregate actions/resources/workspaces for quick comparison
|
|
423
|
+
def aggregate(parts: List[Dict[str, Any]]) -> Dict[str, set]:
|
|
424
|
+
act: set = set()
|
|
425
|
+
res: set = set()
|
|
426
|
+
ws: set = set()
|
|
427
|
+
for st in parts:
|
|
428
|
+
act |= set_from_list(st.get("actions"))
|
|
429
|
+
res |= set_from_list(st.get("resource"))
|
|
430
|
+
w = st.get("workspace")
|
|
431
|
+
if w:
|
|
432
|
+
ws.add(w)
|
|
433
|
+
return {"actions": act, "resources": res, "workspaces": ws}
|
|
434
|
+
|
|
435
|
+
agg1 = aggregate(s1)
|
|
436
|
+
agg2 = aggregate(s2)
|
|
437
|
+
|
|
438
|
+
def show_set_diff(label: str, a: set, b: set) -> None:
|
|
439
|
+
only_a = sorted(a - b)
|
|
440
|
+
only_b = sorted(b - a)
|
|
441
|
+
click.echo(f"\n{label} differences:")
|
|
442
|
+
if not only_a and not only_b:
|
|
443
|
+
click.echo(" No differences")
|
|
444
|
+
return
|
|
445
|
+
if only_a:
|
|
446
|
+
click.echo(" Only in policy 1:")
|
|
447
|
+
for item in only_a:
|
|
448
|
+
click.echo(f" • {item}")
|
|
449
|
+
if only_b:
|
|
450
|
+
click.echo(" Only in policy 2:")
|
|
451
|
+
for item in only_b:
|
|
452
|
+
click.echo(f" • {item}")
|
|
453
|
+
|
|
454
|
+
show_set_diff("Actions", agg1["actions"], agg2["actions"])
|
|
455
|
+
show_set_diff("Resources", agg1["resources"], agg2["resources"])
|
|
456
|
+
show_set_diff("Workspaces", agg1["workspaces"], agg2["workspaces"])
|
|
457
|
+
|
|
458
|
+
except Exception as exc:
|
|
459
|
+
handle_api_error(exc)
|
|
460
|
+
|
|
461
|
+
@template_group.command(name="list")
|
|
462
|
+
@click.option(
|
|
463
|
+
"--type",
|
|
464
|
+
"template_type",
|
|
465
|
+
type=click.Choice(["user", "service"], case_sensitive=False),
|
|
466
|
+
default=None,
|
|
467
|
+
help="Filter by template type",
|
|
468
|
+
)
|
|
469
|
+
@click.option("--builtin", is_flag=True, help="Show built-in templates only")
|
|
470
|
+
@click.option("--name", type=str, default=None, help="Filter by name (contains search)")
|
|
471
|
+
@click.option(
|
|
472
|
+
"--sortby",
|
|
473
|
+
type=click.Choice(["name", "created", "updated"]),
|
|
474
|
+
default="name",
|
|
475
|
+
show_default=True,
|
|
476
|
+
help="Sort field",
|
|
477
|
+
)
|
|
478
|
+
@click.option(
|
|
479
|
+
"--order",
|
|
480
|
+
type=click.Choice(["asc", "desc"]),
|
|
481
|
+
default="asc",
|
|
482
|
+
show_default=True,
|
|
483
|
+
help="Sort order",
|
|
484
|
+
)
|
|
485
|
+
@click.option(
|
|
486
|
+
"--take",
|
|
487
|
+
"-t",
|
|
488
|
+
type=int,
|
|
489
|
+
default=None,
|
|
490
|
+
help="Limit results (default: 25 for table, all for JSON)",
|
|
491
|
+
)
|
|
492
|
+
@click.option(
|
|
493
|
+
"--format",
|
|
494
|
+
"-f",
|
|
495
|
+
type=click.Choice(["table", "json"]),
|
|
496
|
+
default="table",
|
|
497
|
+
show_default=True,
|
|
498
|
+
help="Output format",
|
|
499
|
+
)
|
|
500
|
+
@click.option(
|
|
501
|
+
"--skip",
|
|
502
|
+
"-s",
|
|
503
|
+
type=int,
|
|
504
|
+
default=0,
|
|
505
|
+
show_default=True,
|
|
506
|
+
help="Number of items to skip before returning results",
|
|
507
|
+
)
|
|
508
|
+
def list_templates(
|
|
509
|
+
template_type: Optional[str],
|
|
510
|
+
builtin: bool,
|
|
511
|
+
name: Optional[str],
|
|
512
|
+
sortby: str,
|
|
513
|
+
order: str,
|
|
514
|
+
take: Optional[int],
|
|
515
|
+
format: str,
|
|
516
|
+
skip: int,
|
|
517
|
+
) -> None:
|
|
518
|
+
"""List policy templates with optional filtering."""
|
|
519
|
+
validate_output_format(format)
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
from urllib.parse import urlencode
|
|
523
|
+
|
|
524
|
+
if take is None:
|
|
525
|
+
take = 25 if format == "table" else 100
|
|
526
|
+
|
|
527
|
+
base_url = f"{get_base_url()}/niauth/v1/policy-templates"
|
|
528
|
+
|
|
529
|
+
def build_url(page_take: int, page_skip: int) -> str:
|
|
530
|
+
query_params: Dict[str, Any] = {
|
|
531
|
+
"take": page_take,
|
|
532
|
+
"skip": page_skip,
|
|
533
|
+
"sortby": sortby,
|
|
534
|
+
"order": "ascending" if order == "asc" else "descending",
|
|
535
|
+
}
|
|
536
|
+
if template_type:
|
|
537
|
+
query_params["type"] = template_type.lower()
|
|
538
|
+
if builtin:
|
|
539
|
+
query_params["builtIn"] = "true"
|
|
540
|
+
if name:
|
|
541
|
+
query_params["name"] = f"*{name}*"
|
|
542
|
+
return f"{base_url}?{urlencode(query_params)}"
|
|
543
|
+
|
|
544
|
+
if format == "json":
|
|
545
|
+
import json
|
|
546
|
+
|
|
547
|
+
all_templates: List[Dict[str, Any]] = []
|
|
548
|
+
current_skip = skip
|
|
549
|
+
remaining = take
|
|
550
|
+
|
|
551
|
+
while True:
|
|
552
|
+
page_take = min(remaining, 100) if remaining else 100
|
|
553
|
+
url = build_url(page_take, current_skip)
|
|
554
|
+
resp = make_api_request("GET", url, payload=None)
|
|
555
|
+
data = resp.json()
|
|
556
|
+
templates_page = data.get("policyTemplates", [])
|
|
557
|
+
|
|
558
|
+
if not templates_page:
|
|
559
|
+
break
|
|
560
|
+
|
|
561
|
+
all_templates.extend(templates_page)
|
|
562
|
+
|
|
563
|
+
if remaining:
|
|
564
|
+
if len(all_templates) >= remaining:
|
|
565
|
+
all_templates = all_templates[:remaining]
|
|
566
|
+
break
|
|
567
|
+
remaining -= len(templates_page)
|
|
568
|
+
|
|
569
|
+
if len(templates_page) < page_take:
|
|
570
|
+
break
|
|
571
|
+
|
|
572
|
+
current_skip += page_take
|
|
573
|
+
|
|
574
|
+
click.echo(json.dumps(all_templates, indent=2) if all_templates else "[]")
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
current_skip = skip
|
|
578
|
+
|
|
579
|
+
while True:
|
|
580
|
+
url = build_url(take, current_skip)
|
|
581
|
+
resp = make_api_request("GET", url, payload=None)
|
|
582
|
+
data = resp.json()
|
|
583
|
+
templates = data.get("policyTemplates", [])
|
|
584
|
+
total_count = data.get("totalCount")
|
|
585
|
+
|
|
586
|
+
if not templates and current_skip == skip:
|
|
587
|
+
click.echo("No policy templates found.")
|
|
588
|
+
return
|
|
589
|
+
if not templates:
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
combined_resp = FilteredResponse({"policyTemplates": templates})
|
|
593
|
+
UniversalResponseHandler.handle_list_response(
|
|
594
|
+
resp=combined_resp,
|
|
595
|
+
data_key="policyTemplates",
|
|
596
|
+
item_name="template",
|
|
597
|
+
format_output=format,
|
|
598
|
+
formatter_func=_format_template_list_row,
|
|
599
|
+
headers=["ID", "Name", "Type", "Built-in", "Statements"],
|
|
600
|
+
column_widths=[36, 30, 12, 10, 15],
|
|
601
|
+
enable_pagination=False,
|
|
602
|
+
page_size=take,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if total_count is not None:
|
|
606
|
+
shown = current_skip + len(templates)
|
|
607
|
+
remaining = max(total_count - shown, 0)
|
|
608
|
+
message = f"Showing {len(templates)} of {total_count} policy template(s)."
|
|
609
|
+
if remaining:
|
|
610
|
+
message += f" {remaining} more available."
|
|
611
|
+
click.echo(message)
|
|
612
|
+
|
|
613
|
+
current_skip += take
|
|
614
|
+
|
|
615
|
+
if len(templates) < take:
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
is_tty = sys.stdout.isatty() and sys.stdin.isatty()
|
|
620
|
+
except Exception:
|
|
621
|
+
is_tty = False
|
|
622
|
+
|
|
623
|
+
if not is_tty:
|
|
624
|
+
break
|
|
625
|
+
if not questionary.confirm(f"Show next {take} templates?", default=True).ask():
|
|
626
|
+
break
|
|
627
|
+
|
|
628
|
+
except Exception as exc:
|
|
629
|
+
handle_api_error(exc)
|
|
630
|
+
|
|
631
|
+
@template_group.command(name="get")
|
|
632
|
+
@click.argument("template_id")
|
|
633
|
+
@click.option(
|
|
634
|
+
"--format",
|
|
635
|
+
"-f",
|
|
636
|
+
type=click.Choice(["table", "json"]),
|
|
637
|
+
default="table",
|
|
638
|
+
show_default=True,
|
|
639
|
+
help="Output format",
|
|
640
|
+
)
|
|
641
|
+
def get_template(template_id: str, format: str) -> None:
|
|
642
|
+
"""Get policy template details."""
|
|
643
|
+
validate_output_format(format)
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
url = f"{get_base_url()}/niauth/v1/policy-templates/{template_id}"
|
|
647
|
+
resp = make_api_request("GET", url, payload=None)
|
|
648
|
+
_display_template_details(resp.json(), format)
|
|
649
|
+
except Exception as exc:
|
|
650
|
+
handle_api_error(exc)
|
|
651
|
+
|
|
652
|
+
@template_group.command(name="delete")
|
|
653
|
+
@click.argument("template_id")
|
|
654
|
+
@click.option("--force", is_flag=True, help="Skip confirmation prompt")
|
|
655
|
+
def delete_template(template_id: str, force: bool) -> None:
|
|
656
|
+
"""Delete a policy template."""
|
|
657
|
+
from .utils import check_readonly_mode
|
|
658
|
+
|
|
659
|
+
check_readonly_mode("delete a policy template")
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
if not force:
|
|
663
|
+
details = _fetch_template_details(template_id, handle_errors=False)
|
|
664
|
+
template_name = details.get("name") if details else template_id
|
|
665
|
+
if not questionary.confirm(
|
|
666
|
+
f"Delete policy template '{template_name}'?",
|
|
667
|
+
default=False,
|
|
668
|
+
).ask():
|
|
669
|
+
click.echo("Deletion cancelled.")
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
url = f"{get_base_url()}/niauth/v1/policy-templates/{template_id}"
|
|
673
|
+
resp = make_api_request("DELETE", url, payload=None)
|
|
674
|
+
if resp.status_code not in (200, 204):
|
|
675
|
+
resp.raise_for_status()
|
|
676
|
+
|
|
677
|
+
format_success("Policy template deleted", {"id": template_id})
|
|
678
|
+
except Exception as exc:
|
|
679
|
+
handle_api_error(exc)
|