secflow-cli 0.1.0__tar.gz → 0.2.1__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.
- secflow_cli-0.2.1/PKG-INFO +133 -0
- secflow_cli-0.2.1/USAGE.md +119 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/pyproject.toml +1 -1
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/account_commands.py +18 -9
- secflow_cli-0.2.1/src/secflow_cli/command_support.py +93 -0
- secflow_cli-0.2.1/src/secflow_cli/completion.py +58 -0
- secflow_cli-0.2.1/src/secflow_cli/doctor_commands.py +93 -0
- secflow_cli-0.2.1/src/secflow_cli/help.py +170 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/main.py +97 -3
- secflow_cli-0.2.1/src/secflow_cli/manifest_commands.py +284 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/namespace_commands.py +6 -15
- secflow_cli-0.2.1/src/secflow_cli/operator_commands.py +318 -0
- secflow_cli-0.2.1/src/secflow_cli/policy_audit_commands.py +172 -0
- secflow_cli-0.2.1/src/secflow_cli/reference_commands.py +76 -0
- secflow_cli-0.2.1/src/secflow_cli/report_commands.py +120 -0
- secflow_cli-0.2.1/src/secflow_cli/runtime_commands.py +77 -0
- secflow_cli-0.2.1/src/secflow_cli/secret_commands.py +131 -0
- secflow_cli-0.2.1/src/secflow_cli/session_commands.py +92 -0
- secflow_cli-0.2.1/src/secflow_cli/token_commands.py +99 -0
- secflow_cli-0.2.1/src/secflow_cli.egg-info/PKG-INFO +133 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli.egg-info/SOURCES.txt +12 -0
- secflow_cli-0.1.0/PKG-INFO +0 -64
- secflow_cli-0.1.0/USAGE.md +0 -50
- secflow_cli-0.1.0/src/secflow_cli/help.py +0 -100
- secflow_cli-0.1.0/src/secflow_cli.egg-info/PKG-INFO +0 -64
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/LICENSE +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/setup.cfg +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/__init__.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/__main__.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/access_profile_commands.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/browser_login.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/config.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/credentials.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/errors.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli/role_commands.py +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli.egg-info/entry_points.txt +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/src/secflow_cli.egg-info/requires.txt +0 -0
- {secflow_cli-0.1.0 → secflow_cli-0.2.1}/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.1
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|