secflow-cli 0.1.0__tar.gz → 0.2.0__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 (39) hide show
  1. secflow_cli-0.2.0/PKG-INFO +133 -0
  2. secflow_cli-0.2.0/USAGE.md +119 -0
  3. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/pyproject.toml +1 -1
  4. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/account_commands.py +18 -9
  5. secflow_cli-0.2.0/src/secflow_cli/command_support.py +93 -0
  6. secflow_cli-0.2.0/src/secflow_cli/completion.py +58 -0
  7. secflow_cli-0.2.0/src/secflow_cli/doctor_commands.py +93 -0
  8. secflow_cli-0.2.0/src/secflow_cli/help.py +170 -0
  9. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/main.py +97 -3
  10. secflow_cli-0.2.0/src/secflow_cli/manifest_commands.py +284 -0
  11. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/namespace_commands.py +6 -15
  12. secflow_cli-0.2.0/src/secflow_cli/operator_commands.py +318 -0
  13. secflow_cli-0.2.0/src/secflow_cli/policy_audit_commands.py +172 -0
  14. secflow_cli-0.2.0/src/secflow_cli/reference_commands.py +76 -0
  15. secflow_cli-0.2.0/src/secflow_cli/report_commands.py +120 -0
  16. secflow_cli-0.2.0/src/secflow_cli/runtime_commands.py +77 -0
  17. secflow_cli-0.2.0/src/secflow_cli/secret_commands.py +131 -0
  18. secflow_cli-0.2.0/src/secflow_cli/session_commands.py +92 -0
  19. secflow_cli-0.2.0/src/secflow_cli/token_commands.py +99 -0
  20. secflow_cli-0.2.0/src/secflow_cli.egg-info/PKG-INFO +133 -0
  21. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli.egg-info/SOURCES.txt +12 -0
  22. secflow_cli-0.1.0/PKG-INFO +0 -64
  23. secflow_cli-0.1.0/USAGE.md +0 -50
  24. secflow_cli-0.1.0/src/secflow_cli/help.py +0 -100
  25. secflow_cli-0.1.0/src/secflow_cli.egg-info/PKG-INFO +0 -64
  26. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/LICENSE +0 -0
  27. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/setup.cfg +0 -0
  28. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/__init__.py +0 -0
  29. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/__main__.py +0 -0
  30. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/access_profile_commands.py +0 -0
  31. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/browser_login.py +0 -0
  32. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/config.py +0 -0
  33. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/credentials.py +0 -0
  34. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/errors.py +0 -0
  35. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli/role_commands.py +0 -0
  36. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
  37. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli.egg-info/entry_points.txt +0 -0
  38. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli.egg-info/requires.txt +0 -0
  39. {secflow_cli-0.1.0 → secflow_cli-0.2.0}/src/secflow_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: secflow-cli
3
+ Version: 0.2.0
4
+ Summary: SecFlow operator CLI
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/yeon3724/secflow
7
+ Project-URL: Issues, https://github.com/yeon3724/secflow/issues
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: secflow-sdk==0.1.8
12
+ Requires-Dist: keyring<26,>=25
13
+ Dynamic: license-file
14
+
15
+ # SecFlow CLI
16
+
17
+ Use Intflow browser login or an existing Slack-issued Admin session. Both distributions provide the same `secflowctl` commands.
18
+
19
+ ## Install and update
20
+
21
+ Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
22
+
23
+ ```sh
24
+ pipx install secflow-cli
25
+ # Or in a Python environment:
26
+ python -m pip install secflow-cli
27
+
28
+ # Alternative: Node.js 22+; no separate Python installation required
29
+ npm install --global @intflow/secflow-cli
30
+ secflowctl --version
31
+ ```
32
+
33
+ The Python distribution requires Python 3.10+. Installation automatically includes the Admin-capable `secflow-sdk==0.1.8` dependency.
34
+
35
+ The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Initial targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets yet; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
36
+
37
+ The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
38
+
39
+ ```sh
40
+ pipx upgrade secflow-cli
41
+ # Or use the same installer as the initial installation:
42
+ python -m pip install --upgrade secflow-cli
43
+ npm install --global @intflow/secflow-cli@latest
44
+ ```
45
+
46
+ ## Connect
47
+
48
+ ```sh
49
+ secflowctl login
50
+ secflowctl auth status
51
+ secflowctl namespaces list --help
52
+ secflowctl namespaces list --all
53
+ secflowctl --help
54
+ ```
55
+
56
+ `secflowctl` without arguments displays a quick start. `secflowctl login` opens Intflow login and creates the `production` profile for `https://secflow.intflow.dev` on first use. Existing profiles retain their selected server. For another server, first use `secflowctl profile add staging --base-url https://secflow.example.test`, then `secflowctl --profile staging login`. Global options precede the command. Every command supports `--help`, including examples. Account and role administration requires server authorization.
57
+
58
+ `auth login` remains compatible with Slack-issued Admin sessions and prompts without echo for an existing session. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
59
+
60
+ When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. If login expires or the exchange fails, start a new login.
61
+
62
+ For automation use `SECFLOW_ADMIN_SESSION_TOKEN` supplied by your secret manager, or `--token-stdin` with `--non-interactive`. Never put tokens in command arguments, profiles, shell history, or tracked files. SDK tokens cannot authorize these Admin commands. `auth logout` revokes the current server session; `auth forget` only removes the local credential.
63
+
64
+ To uninstall, use `pipx uninstall secflow-cli`, `python -m pip uninstall secflow-cli`, or `npm uninstall --global @intflow/secflow-cli` as appropriate. Uninstalling does not revoke server sessions or erase profiles; log out and remove unwanted profiles first.
65
+
66
+ ## Expanded commands in 0.2.0
67
+
68
+ CLI 0.2.0 adds the commands below. Metadata catalogs, details, permission checks, and per-secret access changes require the additive operator APIs on the server. An older server can still serve existing commands. `auth status` reports expiry and capabilities on newer servers and falls back to identity-only metadata when the session endpoint is absent. Authorization or dependency failures are not treated as an old server.
69
+
70
+ | Task | Discover commands |
71
+ | --- | --- |
72
+ | Identity, permissions, service state | `auth status`, `auth capabilities`, `health`, `dashboard` |
73
+ | Find authorized targets | `catalog accounts`, `catalog namespaces`, `catalog token-accounts`, `catalog actors` |
74
+ | Account and namespace details | `accounts get --help`, `namespaces get --help` |
75
+ | Namespace members and scoped roles | `namespaces members list`, `add`, `remove`; `roles list --namespace` or `--account-id` |
76
+ | Delete impact | `namespaces impact`, `accounts impact`; `delete` shows the same counts before confirming |
77
+ | Secret lifecycle | `secrets create`, `get`, `list`, `update`, `rotate`, `activate-revision`, `delete`; inspect each with `--help` |
78
+ | Direct secret access overrides | `secrets access list`, `grant`, `block`, `reset`; reset restores inherited access |
79
+ | SDK tokens and Admin sessions | `tokens issue --help`, `tokens revoke --help`, `auth sessions issue --help` |
80
+ | Policies | `policies list`, `get`, `draft`, `preview`, `put`, `templates list`, `templates build` |
81
+ | Audit metadata | `audit events list --help`, `audit exports --help` |
82
+ | SDK access diagnostics | `runtime validate`, `metadata`, `permission-check`, `revoke-leases` |
83
+ | Offline reference preparation | `refs build`, `validate`, `prepare`, `list`, `remove` |
84
+ | Diagnostics | `doctor`; `doctor --watch --interval 60 --count 30` emits one JSONL record per run |
85
+ | Metadata reports | `reports tokens`, `reports rotation`, `reports access --namespace` |
86
+ | Repeatable setup | `manifest export`, `manifest plan`, `manifest apply` (see below) |
87
+ | Shell completion | `completion bash`, `zsh`, or `powershell` |
88
+
89
+ Without saved profiles, `secflowctl health` checks the default production origin without logging in or writing a profile. Use `--base-url` before `health` to select another origin. An explicitly selected profile must exist. Collection commands support bounded pagination where shown in help; audit events have a maximum page size of 100.
90
+
91
+ ## Repeat namespace setup
92
+
93
+ ```sh
94
+ secflowctl manifest export --namespace dev/example > manifest.json
95
+ # Edit owners, audit logging, expiration policy, or members (account_id + secret_reader/secret_writer).
96
+ secflowctl --profile staging manifest plan --file manifest.json > plan.json
97
+ secflowctl --profile staging manifest apply --file plan.json
98
+ ```
99
+
100
+ A manifest covers namespace metadata and namespace members only; it never holds secret values, tokens, or policies, and it never deletes a namespace. `plan` changes nothing; `--prune` also revokes members missing from the manifest. A plan is bound to the server it was made for and cannot be edited. `apply` re-reads each item first: an item already in the desired state is skipped, and an item whose current state differs from the plan stops the run with exit 6. After an interruption or an uncertain result (exit 8), run the same `apply` again; it resumes and retries with each item's original idempotency key. Earlier items stay applied; nothing is rolled back.
101
+
102
+ ## Review and replace a policy
103
+
104
+ First find the active policy with `secflowctl policies list --namespace dev/example`. Then use its ID:
105
+
106
+ ```sh
107
+ secflowctl policies get --namespace dev/example --policy-id POLICY_UUID > current.json
108
+ secflowctl policies draft --file current.json > draft.json
109
+ # Edit draft.json's data.rules, preserving rules that must remain active.
110
+ secflowctl policies preview --file draft.json
111
+ secflowctl policies put --file draft.json
112
+ ```
113
+
114
+ Draft creation is offline. It preserves scope and rules, generates a new policy ID, and increments the version. `put` still submits the supplied policy unchanged: it replaces the complete active policy for that scope. Do not reuse the current policy ID for a new revision. UTF-8, UTF-8 BOM, and BOM-marked UTF-16 files from Windows PowerShell redirection are accepted. Raw API policy JSON and successful CLI get/draft/template-build envelopes are supported.
115
+
116
+ To append a template rule without discarding existing rules:
117
+
118
+ ```sh
119
+ secflowctl policies templates list
120
+ secflowctl policies templates build --file current.json --template runtime_read --account-id ACCOUNT_UUID --ref secret://dev/example/token/primary > draft.json
121
+ secflowctl policies preview --file draft.json
122
+ secflowctl policies put --file draft.json
123
+ ```
124
+
125
+ Template building reads the authorized server catalog and writes only a draft. The SecretRef must belong to the policy namespace; existing deny rules still take precedence. Preview validates a document without saving it and does not prove future authorization or protect against concurrent policy changes. There is no server compare-and-swap precondition: review the latest active policy and coordinate concurrent edits before replacing it.
126
+
127
+ For automation, use `--non-interactive` before the command and `--yes` on `put`. Keep the exact draft file and the idempotency key printed to stderr for an identical retry. Do not generate another draft or key when a mutation outcome is uncertain.
128
+
129
+ ## Credentials and values
130
+
131
+ `tokens issue --save-as NAME` stores an SDK token separately from Admin profiles; `runtime ... --sdk-name NAME` uses it. `auth sessions issue --save-as PROFILE` creates a new destination profile. Both require a new destination name or explicit `--token-stdout` to redirected stdout; raw credentials never appear in ordinary JSON/table results. Linux/macOS non-interactive invocations require an explicitly injected credential rather than unlocking the OS store.
132
+
133
+ Secret create/rotate reads exact UTF-8 or base64 input through hidden input, `--value-stdin`, or `--value-file`. Do not put values in command arguments. Stdin cannot simultaneously carry an Admin token and a secret or policy document. Runtime commands use SDK credentials only; Admin sessions are never substituted. No plaintext resolve command is provided.
@@ -0,0 +1,119 @@
1
+ # SecFlow CLI
2
+
3
+ Use Intflow browser login or an existing Slack-issued Admin session. Both distributions provide the same `secflowctl` commands.
4
+
5
+ ## Install and update
6
+
7
+ Choose one distribution per machine; both own the same `secflowctl` command and share profiles and the OS credential store.
8
+
9
+ ```sh
10
+ pipx install secflow-cli
11
+ # Or in a Python environment:
12
+ python -m pip install secflow-cli
13
+
14
+ # Alternative: Node.js 22+; no separate Python installation required
15
+ npm install --global @intflow/secflow-cli
16
+ secflowctl --version
17
+ ```
18
+
19
+ The Python distribution requires Python 3.10+. Installation automatically includes the Admin-capable `secflow-sdk==0.1.8` dependency.
20
+
21
+ The npm distribution includes Python and the CLI dependencies in OS-specific native packages. Initial targets: Windows x64, macOS Intel/Apple Silicon, and Linux x64 with glibc 2.35+ (Ubuntu 22.04+). Alpine/musl and Linux ARM are not npm targets yet; use the Python distribution there. Keep npm optional dependencies enabled. Installation has no postinstall script or runtime download. Native binaries are not platform-signed or notarized.
22
+
23
+ The bundled interpreter includes CA certificates for HTTPS. An explicit `SSL_CERT_FILE` or `SSL_CERT_DIR` takes precedence for organization-managed trust; TLS certificate verification stays enabled.
24
+
25
+ ```sh
26
+ pipx upgrade secflow-cli
27
+ # Or use the same installer as the initial installation:
28
+ python -m pip install --upgrade secflow-cli
29
+ npm install --global @intflow/secflow-cli@latest
30
+ ```
31
+
32
+ ## Connect
33
+
34
+ ```sh
35
+ secflowctl login
36
+ secflowctl auth status
37
+ secflowctl namespaces list --help
38
+ secflowctl namespaces list --all
39
+ secflowctl --help
40
+ ```
41
+
42
+ `secflowctl` without arguments displays a quick start. `secflowctl login` opens Intflow login and creates the `production` profile for `https://secflow.intflow.dev` on first use. Existing profiles retain their selected server. For another server, first use `secflowctl profile add staging --base-url https://secflow.example.test`, then `secflowctl --profile staging login`. Global options precede the command. Every command supports `--help`, including examples. Account and role administration requires server authorization.
43
+
44
+ `auth login` remains compatible with Slack-issued Admin sessions and prompts without echo for an existing session. Sessions are stored in Windows Credential Manager, macOS Keychain, or Linux Secret Service. Linux persistent login needs an unlocked Secret Service (for example GNOME Keyring) and a D-Bus session. If secure storage is unavailable, login fails without a plaintext file fallback. Installing the CLI does not create an account or grant access.
45
+
46
+ When browser login is enabled on the server, run `secflowctl auth login --browser`. Sign in to Intflow, compare the code with your terminal, and approve the SecFlow login. Use `--browser --no-browser` to open the displayed URL manually. This requires an interactive terminal, an existing authorized SecFlow account, and OS credential storage. The CLI stores only the resulting SecFlow session. If login expires or the exchange fails, start a new login.
47
+
48
+ For automation use `SECFLOW_ADMIN_SESSION_TOKEN` supplied by your secret manager, or `--token-stdin` with `--non-interactive`. Never put tokens in command arguments, profiles, shell history, or tracked files. SDK tokens cannot authorize these Admin commands. `auth logout` revokes the current server session; `auth forget` only removes the local credential.
49
+
50
+ To uninstall, use `pipx uninstall secflow-cli`, `python -m pip uninstall secflow-cli`, or `npm uninstall --global @intflow/secflow-cli` as appropriate. Uninstalling does not revoke server sessions or erase profiles; log out and remove unwanted profiles first.
51
+
52
+ ## Expanded commands in 0.2.0
53
+
54
+ CLI 0.2.0 adds the commands below. Metadata catalogs, details, permission checks, and per-secret access changes require the additive operator APIs on the server. An older server can still serve existing commands. `auth status` reports expiry and capabilities on newer servers and falls back to identity-only metadata when the session endpoint is absent. Authorization or dependency failures are not treated as an old server.
55
+
56
+ | Task | Discover commands |
57
+ | --- | --- |
58
+ | Identity, permissions, service state | `auth status`, `auth capabilities`, `health`, `dashboard` |
59
+ | Find authorized targets | `catalog accounts`, `catalog namespaces`, `catalog token-accounts`, `catalog actors` |
60
+ | Account and namespace details | `accounts get --help`, `namespaces get --help` |
61
+ | Namespace members and scoped roles | `namespaces members list`, `add`, `remove`; `roles list --namespace` or `--account-id` |
62
+ | Delete impact | `namespaces impact`, `accounts impact`; `delete` shows the same counts before confirming |
63
+ | Secret lifecycle | `secrets create`, `get`, `list`, `update`, `rotate`, `activate-revision`, `delete`; inspect each with `--help` |
64
+ | Direct secret access overrides | `secrets access list`, `grant`, `block`, `reset`; reset restores inherited access |
65
+ | SDK tokens and Admin sessions | `tokens issue --help`, `tokens revoke --help`, `auth sessions issue --help` |
66
+ | Policies | `policies list`, `get`, `draft`, `preview`, `put`, `templates list`, `templates build` |
67
+ | Audit metadata | `audit events list --help`, `audit exports --help` |
68
+ | SDK access diagnostics | `runtime validate`, `metadata`, `permission-check`, `revoke-leases` |
69
+ | Offline reference preparation | `refs build`, `validate`, `prepare`, `list`, `remove` |
70
+ | Diagnostics | `doctor`; `doctor --watch --interval 60 --count 30` emits one JSONL record per run |
71
+ | Metadata reports | `reports tokens`, `reports rotation`, `reports access --namespace` |
72
+ | Repeatable setup | `manifest export`, `manifest plan`, `manifest apply` (see below) |
73
+ | Shell completion | `completion bash`, `zsh`, or `powershell` |
74
+
75
+ Without saved profiles, `secflowctl health` checks the default production origin without logging in or writing a profile. Use `--base-url` before `health` to select another origin. An explicitly selected profile must exist. Collection commands support bounded pagination where shown in help; audit events have a maximum page size of 100.
76
+
77
+ ## Repeat namespace setup
78
+
79
+ ```sh
80
+ secflowctl manifest export --namespace dev/example > manifest.json
81
+ # Edit owners, audit logging, expiration policy, or members (account_id + secret_reader/secret_writer).
82
+ secflowctl --profile staging manifest plan --file manifest.json > plan.json
83
+ secflowctl --profile staging manifest apply --file plan.json
84
+ ```
85
+
86
+ A manifest covers namespace metadata and namespace members only; it never holds secret values, tokens, or policies, and it never deletes a namespace. `plan` changes nothing; `--prune` also revokes members missing from the manifest. A plan is bound to the server it was made for and cannot be edited. `apply` re-reads each item first: an item already in the desired state is skipped, and an item whose current state differs from the plan stops the run with exit 6. After an interruption or an uncertain result (exit 8), run the same `apply` again; it resumes and retries with each item's original idempotency key. Earlier items stay applied; nothing is rolled back.
87
+
88
+ ## Review and replace a policy
89
+
90
+ First find the active policy with `secflowctl policies list --namespace dev/example`. Then use its ID:
91
+
92
+ ```sh
93
+ secflowctl policies get --namespace dev/example --policy-id POLICY_UUID > current.json
94
+ secflowctl policies draft --file current.json > draft.json
95
+ # Edit draft.json's data.rules, preserving rules that must remain active.
96
+ secflowctl policies preview --file draft.json
97
+ secflowctl policies put --file draft.json
98
+ ```
99
+
100
+ Draft creation is offline. It preserves scope and rules, generates a new policy ID, and increments the version. `put` still submits the supplied policy unchanged: it replaces the complete active policy for that scope. Do not reuse the current policy ID for a new revision. UTF-8, UTF-8 BOM, and BOM-marked UTF-16 files from Windows PowerShell redirection are accepted. Raw API policy JSON and successful CLI get/draft/template-build envelopes are supported.
101
+
102
+ To append a template rule without discarding existing rules:
103
+
104
+ ```sh
105
+ secflowctl policies templates list
106
+ secflowctl policies templates build --file current.json --template runtime_read --account-id ACCOUNT_UUID --ref secret://dev/example/token/primary > draft.json
107
+ secflowctl policies preview --file draft.json
108
+ secflowctl policies put --file draft.json
109
+ ```
110
+
111
+ Template building reads the authorized server catalog and writes only a draft. The SecretRef must belong to the policy namespace; existing deny rules still take precedence. Preview validates a document without saving it and does not prove future authorization or protect against concurrent policy changes. There is no server compare-and-swap precondition: review the latest active policy and coordinate concurrent edits before replacing it.
112
+
113
+ For automation, use `--non-interactive` before the command and `--yes` on `put`. Keep the exact draft file and the idempotency key printed to stderr for an identical retry. Do not generate another draft or key when a mutation outcome is uncertain.
114
+
115
+ ## Credentials and values
116
+
117
+ `tokens issue --save-as NAME` stores an SDK token separately from Admin profiles; `runtime ... --sdk-name NAME` uses it. `auth sessions issue --save-as PROFILE` creates a new destination profile. Both require a new destination name or explicit `--token-stdout` to redirected stdout; raw credentials never appear in ordinary JSON/table results. Linux/macOS non-interactive invocations require an explicitly injected credential rather than unlocking the OS store.
118
+
119
+ Secret create/rotate reads exact UTF-8 or base64 input through hidden input, `--value-stdin`, or `--value-file`. Do not put values in command arguments. Stdin cannot simultaneously carry an Admin token and a secret or policy document. Runtime commands use SDK credentials only; Admin sessions are never substituted. No plaintext resolve command is provided.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "secflow-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "SecFlow operator CLI"
9
9
  readme = "USAGE.md"
10
10
  license = "MIT"
@@ -9,6 +9,7 @@ from uuid import UUID, uuid4
9
9
 
10
10
  from secflow_sdk import SecFlowConfigError, SecFlowSdkError
11
11
 
12
+ from secflow_cli.command_support import preview_delete, require_confirmable
12
13
  from secflow_cli.errors import CliError
13
14
 
14
15
 
@@ -65,24 +66,32 @@ def prepare(args):
65
66
  values["display_name"] = required_input(args, "display_name", "display name")
66
67
  if args.action == "reactivate":
67
68
  values["status"] = "active"
68
- if args.action in DESTRUCTIVE_ACTIONS:
69
+ if args.action == "delete":
70
+ canonical_account(values["account_id"])
71
+ require_confirmable(args, "Supply the exact account ID and --yes for unattended execution or when stdin carries a token.")
72
+ elif args.action in DESTRUCTIVE_ACTIONS:
69
73
  confirm_destructive(args, values["account_id"])
70
74
  return values
71
75
 
72
76
 
73
- def confirm_destructive(args, account_id):
77
+ def canonical_account(account_id):
74
78
  try:
75
79
  if str(UUID(account_id)) != account_id:
76
80
  raise ValueError
77
81
  except ValueError:
78
82
  raise CliError(2, "INVALID_INPUT", "Supply an exact canonical account UUID.") from None
79
- print(args.action.capitalize() + " account: " + account_id, file=sys.stderr)
80
- impact = (
81
- "suspension revokes active Admin sessions and SDK tokens. Reactivation does not restore revoked credentials."
82
- if args.action == "suspend" else
83
- "deletion removes the account, its sessions, SDK tokens, and direct role bindings."
84
- )
85
- print("Operation semantics, not a server impact preview: " + impact + " Affected counts are unknown.", file=sys.stderr)
83
+
84
+
85
+ def preview(args, client, values):
86
+ return preview_delete(args, client, kind="account", target=values["account_id"], operation="accounts.impact",
87
+ query={"account_id": values["account_id"]},
88
+ semantics="deletion removes the account, its sessions, SDK tokens, and direct role bindings.")
89
+
90
+
91
+ def confirm_destructive(args, account_id):
92
+ canonical_account(account_id)
93
+ print("Suspend account: " + account_id, file=sys.stderr)
94
+ print("Operation semantics, not a server impact preview: suspension revokes active Admin sessions and SDK tokens. Reactivation does not restore revoked credentials. Affected counts are unknown.", file=sys.stderr)
86
95
  if args.yes:
87
96
  return
88
97
  if args.non_interactive or not sys.stdin.isatty() or args.token_stdin:
@@ -0,0 +1,93 @@
1
+ """Shared validation for operator commands; never render request payloads."""
2
+ import hashlib
3
+ import re
4
+ import sys
5
+ from uuid import UUID, uuid4
6
+
7
+ from secflow_cli.errors import CliError
8
+
9
+
10
+ def mutation_options(command, *, confirm=False):
11
+ command.add_argument('--token-stdin', action='store_true')
12
+ command.add_argument('--idempotency-key')
13
+ if confirm:
14
+ command.add_argument('--yes', action='store_true')
15
+
16
+
17
+ def mutation_key(args):
18
+ key = args.idempotency_key or 'cli-operation-' + str(uuid4())
19
+ if not re.fullmatch(r'[A-Za-z0-9_.:-]{16,128}', key):
20
+ raise CliError(2, 'INVALID_INPUT', 'Idempotency keys must contain 16-128 normalized characters.')
21
+ return key
22
+
23
+
24
+ def correlation(key):
25
+ digest = hashlib.sha256(key.encode('ascii')).hexdigest()
26
+ return dict(request_id='cli-request-' + digest, trace_id='cli-trace-' + digest)
27
+
28
+
29
+ def canonical_id(value):
30
+ try:
31
+ if str(UUID(value)) == value:
32
+ return value
33
+ except (ValueError, TypeError, AttributeError):
34
+ pass
35
+ raise CliError(2, 'INVALID_INPUT', 'Supply a canonical resource UUID.')
36
+
37
+
38
+ def confirm(args, target):
39
+ if args.yes:
40
+ return
41
+ if args.non_interactive or args.token_stdin or not sys.stdin.isatty():
42
+ raise CliError(2, 'CONFIRMATION_REQUIRED', 'Inspect the exact target, then supply --yes to confirm this change.')
43
+ print('Type the exact target to confirm: ' + target, file=sys.stderr)
44
+ try:
45
+ answer = input()
46
+ except EOFError:
47
+ answer = None
48
+ if answer != target:
49
+ raise CliError(2, 'CONFIRMATION_CANCELLED', 'Cancelled; no mutation was sent.')
50
+
51
+
52
+ BLOCKING = frozenset({'current_account'})
53
+
54
+
55
+ def require_confirmable(args, message):
56
+ if not args.yes and (args.non_interactive or args.token_stdin or not sys.stdin.isatty()):
57
+ raise CliError(2, 'CONFIRMATION_REQUIRED', message)
58
+
59
+
60
+ def server_impact(client, operation, query):
61
+ from secflow_sdk import SecFlowSdkError
62
+ try:
63
+ response = client._request_json('GET', '/v1/admin/operator/' + operation, query=tuple(query.items()))
64
+ except SecFlowSdkError:
65
+ return None
66
+ impact, blocking = response.get('impact'), response.get('blocking')
67
+ if (not isinstance(impact, dict) or not isinstance(blocking, list)
68
+ or not all(isinstance(key, str) and (value is None or type(value) is int) for key, value in impact.items())
69
+ or not all(isinstance(item, str) for item in blocking)):
70
+ return None
71
+ return {'impact': dict(impact), 'blocking': [item if item in BLOCKING else 'unknown_precondition' for item in blocking]}
72
+
73
+
74
+ def preview_delete(args, client, *, kind, target, operation, query, semantics):
75
+ """Show the server's current-state impact, then require exact confirmation."""
76
+ print('Delete ' + kind + ': ' + target, file=sys.stderr)
77
+ impact = server_impact(client, operation, query)
78
+ if impact is None:
79
+ print('Operation semantics, not a server impact preview: ' + semantics + ' Affected counts are unknown.', file=sys.stderr)
80
+ else:
81
+ counts = ', '.join(key + '=' + ('not counted' if value is None else str(value)) for key, value in impact['impact'].items())
82
+ print('Server impact preview of current state (not a reservation): ' + counts, file=sys.stderr)
83
+ if impact['blocking']:
84
+ raise CliError(6, 'PRECONDITION_FAILED', 'The server reports a blocking precondition (' + ', '.join(impact['blocking']) + '); no mutation was sent.')
85
+ if not args.yes:
86
+ print('Type the ' + kind + ' to confirm: ', end='', file=sys.stderr, flush=True)
87
+ try:
88
+ answer = input()
89
+ except EOFError:
90
+ answer = None
91
+ if answer != target:
92
+ raise CliError(2, 'CONFIRMATION_CANCELLED', kind.capitalize() + ' deletion cancelled; no mutation was sent.')
93
+ return impact
@@ -0,0 +1,58 @@
1
+ """Offline shell completion generated from the parser; never fetches server catalogs."""
2
+ import argparse
3
+ import json
4
+
5
+
6
+ def add_commands(groups):
7
+ completion = groups.add_parser('completion', help='Print a shell completion script (bash, zsh, or PowerShell).')
8
+ completion.add_argument('shell', choices=('bash', 'zsh', 'powershell'))
9
+
10
+
11
+ def tree(parser, path=()):
12
+ words = {}
13
+ options = [option for action in parser._actions for option in action.option_strings if option.startswith('--')]
14
+ children = {}
15
+ for action in parser._actions:
16
+ if isinstance(action, argparse._SubParsersAction):
17
+ children = action.choices
18
+ words[' '.join(path)] = sorted(children) + sorted(options)
19
+ for name, child in children.items():
20
+ words.update(tree(child, (*path, name)))
21
+ return words
22
+
23
+
24
+ BASH = r'''_secflowctl() {
25
+ local cur="${COMP_WORDS[COMP_CWORD]}" path="" word i
26
+ for ((i = 1; i < COMP_CWORD; i++)); do
27
+ word="${COMP_WORDS[i]}"
28
+ [[ "$word" == -* ]] && continue
29
+ case " ${_SECFLOWCTL_WORDS[${path:-_}]} " in *" $word "*) path="${path:+$path }$word" ;; esac
30
+ done
31
+ COMPREPLY=($(compgen -W "${_SECFLOWCTL_WORDS[${path:-_}]}" -- "$cur"))
32
+ }
33
+ complete -F _secflowctl secflowctl
34
+ '''
35
+
36
+ POWERSHELL = r'''Register-ArgumentCompleter -Native -CommandName secflowctl -ScriptBlock {
37
+ param($wordToComplete, $commandAst, $cursorPosition)
38
+ $path = ''
39
+ foreach ($element in $commandAst.CommandElements | Select-Object -Skip 1) {
40
+ $word = $element.ToString()
41
+ if ($word -eq $wordToComplete -or $word.StartsWith('-')) { continue }
42
+ if ($SecflowctlWords[$path] -contains $word) { $path = ($path + ' ' + $word).Trim() }
43
+ }
44
+ $SecflowctlWords[$path] | Where-Object { $_ -like "$wordToComplete*" } |
45
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
46
+ }
47
+ '''
48
+
49
+
50
+ def script(shell, parser):
51
+ words = tree(parser)
52
+ if shell == 'powershell':
53
+ entries = '\n'.join(' ' + json.dumps(key) + ' = @(' + ', '.join("'" + word + "'" for word in value) + ')'
54
+ for key, value in sorted(words.items()))
55
+ return '$SecflowctlWords = @{\n' + entries + '\n}\n' + POWERSHELL
56
+ entries = '\n'.join(' [' + json.dumps(key or '_') + ']=' + json.dumps(' '.join(value)) for key, value in sorted(words.items()))
57
+ prefix = 'autoload -U +X bashcompinit && bashcompinit\n' if shell == 'zsh' else ''
58
+ return prefix + 'declare -A _SECFLOWCTL_WORDS=(\n' + entries + '\n)\n' + BASH
@@ -0,0 +1,93 @@
1
+ """Bounded connection and credential diagnostics; health never proves secret resolution."""
2
+ from datetime import datetime, timedelta, timezone
3
+ import sys
4
+ import time
5
+
6
+ from secflow_sdk import SecFlowHttpError, SecFlowSdkError
7
+ from secflow_cli.errors import CliError
8
+
9
+
10
+ def add_commands(groups):
11
+ doctor = groups.add_parser('doctor', help='Diagnose the selected profile, credential, server health, and session.')
12
+ doctor.add_argument('--token-stdin', action='store_true')
13
+ doctor.add_argument('--watch', action='store_true', help='Repeat the diagnosis; each run is one JSONL record.')
14
+ doctor.add_argument('--interval', type=float, default=30.0, help='Seconds between runs with --watch, 5-3600 (default: 30).')
15
+ doctor.add_argument('--count', type=int, default=10, help='Number of runs with --watch, 1-1000 (default: 10).')
16
+ doctor.set_defaults(jsonl_ok=True)
17
+
18
+
19
+ def check(name, status, detail):
20
+ return {'check': name, 'status': status, 'detail': detail}
21
+
22
+
23
+ def session_checks(client, now):
24
+ try:
25
+ session = client._request_json('GET', '/v1/admin/operator/session')
26
+ except SecFlowHttpError as error:
27
+ if error.status_code == 404:
28
+ client.get_current_account()
29
+ return [check('session', 'ok', 'Admin session accepted.'),
30
+ check('server_features', 'warn', 'The server predates operator metadata; newer commands are unavailable.')]
31
+ if error.status_code in (401, 403):
32
+ return [check('session', 'fail', 'The server rejected the Admin session; sign in again.')]
33
+ raise
34
+ expires = session.get('expires_at')
35
+ try:
36
+ remaining = datetime.fromisoformat(expires.replace('Z', '+00:00')) - now
37
+ except (AttributeError, TypeError, ValueError):
38
+ return [check('session', 'warn', 'Admin session accepted; expiry is unknown.')]
39
+ if remaining <= timedelta(0):
40
+ return [check('session', 'fail', 'The Admin session has expired.')]
41
+ status = 'warn' if remaining < timedelta(days=1) else 'ok'
42
+ return [check('session', status, 'Admin session accepted; expires at ' + expires + '.'),
43
+ check('server_features', 'ok', 'Operator metadata is available.')]
44
+
45
+
46
+ def diagnose(args, name, url, client_factory, health):
47
+ checks = [check('profile', 'ok', name + ' -> ' + url)]
48
+ result, error = health(url, args.timeout)
49
+ checks.append(check('health', 'fail' if error else 'ok',
50
+ 'healthz=' + result['healthz']['status'] + ', readyz=' + result['readyz']['status']))
51
+ try:
52
+ client = client_factory()
53
+ except CliError as caught:
54
+ checks.append(check('credential', 'fail', caught.message))
55
+ return checks
56
+ checks.append(check('credential', 'ok', 'An Admin session is available (value not shown).'))
57
+ try:
58
+ checks += session_checks(client, datetime.now(timezone.utc))
59
+ except SecFlowSdkError:
60
+ checks.append(check('session', 'fail', 'The session could not be verified; the server request did not complete.'))
61
+ return checks
62
+
63
+
64
+ def run(args, name, url, client_factory, command, output):
65
+ from secflow_cli.main import envelope, health, render
66
+ if args.watch and (not 5 <= args.interval <= 3600 or not 1 <= args.count <= 1000):
67
+ raise CliError(2, 'INVALID_INPUT', 'Interval must be 5-3600 seconds and count 1-1000.')
68
+ runs = args.count if args.watch else 1
69
+ output = 'jsonl' if args.watch and output == 'json' else output
70
+ cache = []
71
+
72
+ def cached_client():
73
+ # stdin carries the credential once; later runs reuse the same client.
74
+ if not cache:
75
+ try:
76
+ cache.append(client_factory())
77
+ except CliError as error:
78
+ cache.append(error)
79
+ if isinstance(cache[0], CliError):
80
+ raise cache[0]
81
+ return cache[0]
82
+
83
+ status = 0
84
+ for index in range(runs):
85
+ checks = diagnose(args, name, url, cached_client, health)
86
+ failed = any(item['status'] == 'fail' for item in checks)
87
+ error = CliError(7, 'DIAGNOSIS_FAILED', 'One or more checks failed.') if failed else None
88
+ render(envelope(command, checks, error=error), output)
89
+ status = error.status if error else 0
90
+ if index + 1 < runs:
91
+ sys.stdout.flush()
92
+ time.sleep(args.interval)
93
+ return status