systemlink-cli 1.6.2__tar.gz → 1.6.3__tar.gz

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 (76) hide show
  1. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/PKG-INFO +1 -1
  2. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/pyproject.toml +1 -1
  3. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/user_click.py +99 -0
  5. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/utils.py +1 -1
  6. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/LICENSE +0 -0
  7. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/dff-editor/editor.js +0 -0
  8. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/dff-editor/index.html +0 -0
  9. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/__init__.py +0 -0
  10. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/__main__.py +0 -0
  11. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/asset_click.py +0 -0
  12. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/cli_formatters.py +0 -0
  13. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/cli_utils.py +0 -0
  14. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/comment_click.py +0 -0
  15. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/completion_click.py +0 -0
  16. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/config.py +0 -0
  17. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/config_click.py +0 -0
  18. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/dff_click.py +0 -0
  19. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/dff_decorators.py +0 -0
  20. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/example_click.py +0 -0
  21. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/example_loader.py +0 -0
  22. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/example_provisioner.py +0 -0
  23. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/README.md +0 -0
  24. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/_schema/schema-v1.0.json +0 -0
  25. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/README.md +0 -0
  26. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  27. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/demo-test-plans/README.md +0 -0
  28. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/demo-test-plans/config.yaml +0 -0
  29. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  30. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  31. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  32. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  33. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  34. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  35. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  36. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  37. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  38. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  39. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/feed_click.py +0 -0
  40. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/file_click.py +0 -0
  41. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/function_click.py +0 -0
  42. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/function_templates.py +0 -0
  43. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/main.py +0 -0
  44. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/mcp_click.py +0 -0
  45. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/mcp_server.py +0 -0
  46. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/notebook_click.py +0 -0
  47. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/platform.py +0 -0
  48. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/policy_click.py +0 -0
  49. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/policy_utils.py +0 -0
  50. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/profiles.py +0 -0
  51. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/response_handlers.py +0 -0
  52. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/rich_output.py +0 -0
  53. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/routine_click.py +0 -0
  54. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skill_click.py +0 -0
  55. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/slcli/SKILL.md +0 -0
  56. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  57. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/slcli/references/filtering.md +0 -0
  58. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
  59. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  60. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
  61. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
  62. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  63. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/ssl_trust.py +0 -0
  64. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/system_click.py +0 -0
  65. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/table_utils.py +0 -0
  66. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/tag_click.py +0 -0
  67. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/templates_click.py +0 -0
  68. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/testmonitor_click.py +0 -0
  69. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/universal_handlers.py +0 -0
  70. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/web_editor.py +0 -0
  71. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/webapp_click.py +0 -0
  72. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/workflow_preview.py +0 -0
  73. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/workflows_click.py +0 -0
  74. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/workitem_click.py +0 -0
  75. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/workspace_click.py +0 -0
  76. {systemlink_cli-1.6.2 → systemlink_cli-1.6.3}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.6.2
3
+ Version: 1.6.3
4
4
  Summary: SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates.
5
5
  License-File: LICENSE
6
6
  Author: Fred Visser
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.6.2"
3
+ version = "1.6.3"
4
4
  description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
5
5
  authors = ["Fred Visser <fred.visser@emerson.com>"]
6
6
  packages = [{ include = "slcli" }]
@@ -1,4 +1,4 @@
1
1
  """Version information for slcli."""
2
2
 
3
3
  # This file is auto-generated. Do not edit manually.
4
- __version__ = "1.6.2"
4
+ __version__ = "1.6.3"
@@ -29,6 +29,103 @@ from .workspace_utils import get_workspace_display_name, resolve_workspace_id
29
29
 
30
30
  USER_QUERY_PAGE_SIZE = 100
31
31
  USER_JSON_DEFAULT_TAKE = 1000
32
+ AUTH_WILDCARD_VALUES = {"*", "*/*", "*:*"}
33
+ AUTH_MUTATING_VERBS = {"*", "create", "write", "update", "manage", "admin", "delete"}
34
+ AUTH_ADMIN_RESOURCE_KEYWORDS = {"policy", "policies", "role", "roles"}
35
+
36
+
37
+ def _has_global_auth_scope(value: Any) -> bool:
38
+ """Return whether an auth scope value grants wildcard access."""
39
+ if isinstance(value, str):
40
+ return value.strip().lower() in AUTH_WILDCARD_VALUES
41
+ if isinstance(value, list):
42
+ return any(
43
+ isinstance(item, str) and item.strip().lower() in AUTH_WILDCARD_VALUES for item in value
44
+ )
45
+ return False
46
+
47
+
48
+ def _action_grants_auth_management(action: str) -> bool:
49
+ """Return whether an action implies auth policy or role management access."""
50
+ normalized = action.strip().lower()
51
+ if normalized in AUTH_WILDCARD_VALUES:
52
+ return True
53
+
54
+ action_parts = [part.strip().lower() for part in normalized.split(":") if part.strip()]
55
+ if len(action_parts) < 2 or action_parts[0] not in {"niauth", "auth"}:
56
+ return False
57
+
58
+ remainder = action_parts[1:]
59
+ if remainder == ["*"]:
60
+ return True
61
+
62
+ if remainder[-1] not in AUTH_MUTATING_VERBS and "*" not in remainder:
63
+ return False
64
+
65
+ if len(remainder) == 1:
66
+ return True
67
+
68
+ return any(part in AUTH_ADMIN_RESOURCE_KEYWORDS for part in remainder[:-1])
69
+
70
+
71
+ def _has_user_admin_access(auth_context: Dict[str, Any]) -> bool:
72
+ """Return whether the current caller appears to have server-admin style access."""
73
+ for policy in auth_context.get("policies", []):
74
+ statements = policy.get("statements", [])
75
+ if not isinstance(statements, list):
76
+ continue
77
+
78
+ for statement in statements:
79
+ if not isinstance(statement, dict):
80
+ continue
81
+
82
+ if not _has_global_auth_scope(statement.get("workspace")):
83
+ continue
84
+ if not _has_global_auth_scope(statement.get("resource")):
85
+ continue
86
+
87
+ actions = statement.get("actions", [])
88
+ if not isinstance(actions, list):
89
+ continue
90
+
91
+ for action in actions:
92
+ if isinstance(action, str) and _action_grants_auth_management(action):
93
+ return True
94
+
95
+ return False
96
+
97
+
98
+ def _try_get_current_auth_context() -> Optional[Dict[str, Any]]:
99
+ """Fetch effective auth data for the current caller when available."""
100
+ url = f"{get_base_url()}/niauth/v1/auth"
101
+
102
+ try:
103
+ auth_response = make_api_request("GET", url, payload=None, handle_errors=False)
104
+ auth_context = auth_response.json()
105
+ return auth_context if isinstance(auth_context, dict) else None
106
+ except Exception as exc:
107
+ error_response = getattr(exc, "response", None)
108
+ if error_response is not None and error_response.status_code in {401, 403}:
109
+ handle_api_error(exc)
110
+ return None
111
+
112
+
113
+ def _ensure_user_admin_access(operation: str) -> None:
114
+ """Fail fast when the caller clearly lacks admin access for user mutations."""
115
+ auth_context = _try_get_current_auth_context()
116
+ if auth_context is None:
117
+ return
118
+
119
+ if _has_user_admin_access(auth_context):
120
+ return
121
+
122
+ click.echo(
123
+ f"✗ User and service account {operation} requires server admin permissions. "
124
+ "The active API key does not appear to have wildcard auth role or policy access "
125
+ "across all workspaces.",
126
+ err=True,
127
+ )
128
+ sys.exit(ExitCodes.PERMISSION_DENIED)
32
129
 
33
130
 
34
131
  def _get_policy_details(policy_id: str) -> Optional[dict]:
@@ -851,6 +948,7 @@ def register_user_commands(cli: click.Group) -> None:
851
948
  from .utils import check_readonly_mode
852
949
 
853
950
  check_readonly_mode("create a user")
951
+ _ensure_user_admin_access("creation")
854
952
 
855
953
  # If user_type wasn't specified via CLI, prompt for it first
856
954
  if user_type is None:
@@ -1060,6 +1158,7 @@ def register_user_commands(cli: click.Group) -> None:
1060
1158
  from .utils import check_readonly_mode
1061
1159
 
1062
1160
  check_readonly_mode("update a user")
1161
+ _ensure_user_admin_access("updates")
1063
1162
 
1064
1163
  # First, fetch the user to check if it's a service account
1065
1164
  get_url = f"{get_base_url()}/niuser/v1/users/{user_id}"
@@ -75,7 +75,7 @@ def handle_api_error(exc: Exception) -> None:
75
75
  if "not found" in error_msg:
76
76
  click.echo(f"✗ Resource not found: {exc}", err=True)
77
77
  sys.exit(ExitCodes.NOT_FOUND)
78
- elif "permission" in error_msg or "unauthorized" in error_msg:
78
+ elif "permission" in error_msg or "unauthorized" in error_msg or "forbidden" in error_msg:
79
79
  click.echo(f"✗ Permission denied: {exc}", err=True)
80
80
  sys.exit(ExitCodes.PERMISSION_DENIED)
81
81
  elif "network" in error_msg or "connection" in error_msg:
File without changes