secflow-cli 0.2.4__tar.gz → 0.2.5__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.4/src/secflow_cli.egg-info → secflow_cli-0.2.5}/PKG-INFO +7 -7
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/USAGE.md +6 -6
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/pyproject.toml +1 -1
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/browser_login.py +52 -14
- secflow_cli-0.2.5/src/secflow_cli/logkit/__init__.py +976 -0
- secflow_cli-0.2.5/src/secflow_cli/logkit/test_logkit.py +1282 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/main.py +8 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/manifest_commands.py +5 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/session_commands.py +8 -4
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/token_commands.py +12 -4
- {secflow_cli-0.2.4 → secflow_cli-0.2.5/src/secflow_cli.egg-info}/PKG-INFO +7 -7
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli.egg-info/SOURCES.txt +3 -1
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/LICENSE +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/setup.cfg +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/__init__.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/__main__.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/access_profile_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/account_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/command_support.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/completion.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/config.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/credentials.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/doctor_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/errors.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/help.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/namespace_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/operator_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/output.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/paging.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/policy_audit_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/reference_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/report_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/role_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/runtime_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli/secret_commands.py +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli.egg-info/dependency_links.txt +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli.egg-info/entry_points.txt +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli.egg-info/requires.txt +0 -0
- {secflow_cli-0.2.4 → secflow_cli-0.2.5}/src/secflow_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secflow-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: SecFlow operator CLI
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/yeon3724/secflow
|
|
@@ -14,7 +14,7 @@ Dynamic: license-file
|
|
|
14
14
|
|
|
15
15
|
# SecFlow CLI
|
|
16
16
|
|
|
17
|
-
Use Intflow browser login or an existing
|
|
17
|
+
Use Intflow browser login or an existing Admin session token. Both distributions provide the same `secflowctl` commands.
|
|
18
18
|
|
|
19
19
|
## Install and update
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ secflowctl --version
|
|
|
32
32
|
|
|
33
33
|
The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
|
|
34
34
|
|
|
35
|
-
The npm distribution includes Python and the CLI dependencies in OS-specific native packages.
|
|
35
|
+
The npm distribution includes Python and the CLI dependencies in OS-specific native packages. 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; 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
36
|
|
|
37
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
38
|
|
|
@@ -55,9 +55,9 @@ secflowctl --help
|
|
|
55
55
|
|
|
56
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
57
|
|
|
58
|
-
`auth login`
|
|
58
|
+
`auth login` prompts without echo for an existing Admin session token. 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
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,
|
|
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. A busy server is retried for a minute. If login expires or the exchange fails, log in again.
|
|
61
61
|
|
|
62
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
63
|
|
|
@@ -128,6 +128,6 @@ For automation, use `--non-interactive` before the command and `--yes` on `put`.
|
|
|
128
128
|
|
|
129
129
|
## Credentials and values
|
|
130
130
|
|
|
131
|
-
`tokens issue
|
|
131
|
+
`tokens issue` uses the server-default lifetime; choose `--ttl-seconds` (1-7776000) or `--no-expiry`, never both. `--save-as NAME` stores an SDK token for `runtime ... --sdk-name NAME`. `auth sessions issue --save-as PROFILE` stores a separate Admin profile. Both need a new destination or `--token-stdout` redirected to a pipe/file; ordinary results contain no raw credentials. Linux/macOS non-interactive calls need an injected credential, not an OS store unlock.
|
|
132
132
|
|
|
133
|
-
Secret create/rotate
|
|
133
|
+
Secret create/rotate accepts exact UTF-8 or base64 via hidden input, `--value-stdin` or `--value-file`, never command arguments. Stdin cannot carry both an Admin token and a value or policy. Runtime uses SDK credentials, never Admin sessions; no plaintext resolve command exists.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SecFlow CLI
|
|
2
2
|
|
|
3
|
-
Use Intflow browser login or an existing
|
|
3
|
+
Use Intflow browser login or an existing Admin session token. Both distributions provide the same `secflowctl` commands.
|
|
4
4
|
|
|
5
5
|
## Install and update
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ secflowctl --version
|
|
|
18
18
|
|
|
19
19
|
The Python distribution requires Python 3.10+. Installation automatically includes the matching Admin-capable `secflow-sdk` dependency.
|
|
20
20
|
|
|
21
|
-
The npm distribution includes Python and the CLI dependencies in OS-specific native packages.
|
|
21
|
+
The npm distribution includes Python and the CLI dependencies in OS-specific native packages. 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; 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
22
|
|
|
23
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
24
|
|
|
@@ -41,9 +41,9 @@ secflowctl --help
|
|
|
41
41
|
|
|
42
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
43
|
|
|
44
|
-
`auth login`
|
|
44
|
+
`auth login` prompts without echo for an existing Admin session token. 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
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,
|
|
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. A busy server is retried for a minute. If login expires or the exchange fails, log in again.
|
|
47
47
|
|
|
48
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
49
|
|
|
@@ -114,6 +114,6 @@ For automation, use `--non-interactive` before the command and `--yes` on `put`.
|
|
|
114
114
|
|
|
115
115
|
## Credentials and values
|
|
116
116
|
|
|
117
|
-
`tokens issue
|
|
117
|
+
`tokens issue` uses the server-default lifetime; choose `--ttl-seconds` (1-7776000) or `--no-expiry`, never both. `--save-as NAME` stores an SDK token for `runtime ... --sdk-name NAME`. `auth sessions issue --save-as PROFILE` stores a separate Admin profile. Both need a new destination or `--token-stdout` redirected to a pipe/file; ordinary results contain no raw credentials. Linux/macOS non-interactive calls need an injected credential, not an OS store unlock.
|
|
118
118
|
|
|
119
|
-
Secret create/rotate
|
|
119
|
+
Secret create/rotate accepts exact UTF-8 or base64 via hidden input, `--value-stdin` or `--value-file`, never command arguments. Stdin cannot carry both an Admin token and a value or policy. Runtime uses SDK credentials, never Admin sessions; no plaintext resolve command exists.
|
|
@@ -4,6 +4,7 @@ import base64
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import sys
|
|
@@ -11,12 +12,18 @@ import time
|
|
|
11
12
|
from urllib.error import HTTPError
|
|
12
13
|
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
13
14
|
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
18
|
import webbrowser
|
|
15
19
|
|
|
16
20
|
from secflow_sdk import SecFlowAdminClient
|
|
17
21
|
from secflow_cli.errors import CliError
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
BUSY_WAIT_SECONDS = 60
|
|
25
|
+
|
|
26
|
+
|
|
20
27
|
def rejected():
|
|
21
28
|
return CliError(3, "BROWSER_LOGIN_FAILED", "Browser login did not complete. Start a new login; uncertain session exchanges are not retried.")
|
|
22
29
|
|
|
@@ -32,9 +39,9 @@ def post(url, body, timeout):
|
|
|
32
39
|
try:
|
|
33
40
|
response = build_opener(NoRedirect).open(request, timeout=timeout)
|
|
34
41
|
except HTTPError as error:
|
|
35
|
-
status = error.code
|
|
42
|
+
status, retry_after = error.code, _retry_after(error.headers.get("Retry-After"))
|
|
36
43
|
error.close()
|
|
37
|
-
return status, {}
|
|
44
|
+
return status, {}, retry_after
|
|
38
45
|
with response:
|
|
39
46
|
raw = response.read(16385)
|
|
40
47
|
if len(raw) > 16384:
|
|
@@ -42,7 +49,13 @@ def post(url, body, timeout):
|
|
|
42
49
|
value = json.loads(raw)
|
|
43
50
|
if not isinstance(value, dict):
|
|
44
51
|
raise rejected()
|
|
45
|
-
return response.status, value
|
|
52
|
+
return response.status, value, None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _retry_after(value):
|
|
56
|
+
if not isinstance(value, str) or not re.fullmatch(r"[0-9]{1,4}", value):
|
|
57
|
+
return None
|
|
58
|
+
return min(30, max(1, int(value)))
|
|
46
59
|
|
|
47
60
|
|
|
48
61
|
def acquire_session(url, timeout, *, no_browser=False):
|
|
@@ -57,7 +70,8 @@ def acquire_session(url, timeout, *, no_browser=False):
|
|
|
57
70
|
def _open_browser(verification_uri):
|
|
58
71
|
try:
|
|
59
72
|
opened = webbrowser.open(verification_uri)
|
|
60
|
-
except Exception:
|
|
73
|
+
except Exception as caught:
|
|
74
|
+
LOGGER.debug("Browser could not be opened", extra={"attributes": {"error.type": type(caught).__name__}})
|
|
61
75
|
opened = False
|
|
62
76
|
if not opened:
|
|
63
77
|
print("Open the URL above manually to continue.", file=sys.stderr)
|
|
@@ -75,10 +89,26 @@ def _issued_session(result):
|
|
|
75
89
|
return {field: result[field] for field in ("session_token", "session_id", "account_id", "expires_at")}
|
|
76
90
|
|
|
77
91
|
|
|
92
|
+
def _create(url, proof, timeout):
|
|
93
|
+
give_up = time.monotonic() + BUSY_WAIT_SECONDS
|
|
94
|
+
announced = False
|
|
95
|
+
while True:
|
|
96
|
+
started_at = time.monotonic()
|
|
97
|
+
status, created, retry_after = post(url + "/v1/auth/browser-login", {"poll_challenge": hashlib.sha256(proof.encode()).hexdigest()}, timeout)
|
|
98
|
+
if status != 503 or retry_after is None:
|
|
99
|
+
return status, created, started_at
|
|
100
|
+
remaining = give_up - time.monotonic()
|
|
101
|
+
if remaining <= 0:
|
|
102
|
+
raise CliError(3, "BROWSER_LOGIN_BUSY", "The server is too busy to start a browser login. Try again in a few minutes.")
|
|
103
|
+
if not announced:
|
|
104
|
+
print("The server is busy. Retrying for up to " + str(BUSY_WAIT_SECONDS) + " seconds.", file=sys.stderr)
|
|
105
|
+
announced = True
|
|
106
|
+
time.sleep(min(retry_after, remaining))
|
|
107
|
+
|
|
108
|
+
|
|
78
109
|
def _acquire_session(url, timeout, *, no_browser):
|
|
79
110
|
proof = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode("ascii")
|
|
80
|
-
started_at =
|
|
81
|
-
status, created = post(url + "/v1/auth/browser-login", {"poll_challenge": hashlib.sha256(proof.encode()).hexdigest()}, timeout)
|
|
111
|
+
status, created, started_at = _create(url, proof, timeout)
|
|
82
112
|
if status == 404:
|
|
83
113
|
raise CliError(3, "BROWSER_LOGIN_UNAVAILABLE", "This server has not enabled browser login. Use a Slack-issued Admin session.")
|
|
84
114
|
if status != 201:
|
|
@@ -97,18 +127,26 @@ def _acquire_session(url, timeout, *, no_browser):
|
|
|
97
127
|
print("Confirm code " + created["user_code"] + " and approve the login in your browser.", file=sys.stderr)
|
|
98
128
|
if not no_browser:
|
|
99
129
|
_open_browser(verification_uri)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
return _poll(url, timeout, identifier, proof, deadline, created["interval"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _poll(url, timeout, identifier, proof, deadline, interval):
|
|
134
|
+
wait = interval
|
|
135
|
+
while time.monotonic() + wait < deadline:
|
|
136
|
+
time.sleep(wait)
|
|
137
|
+
wait = interval
|
|
103
138
|
remaining = deadline - time.monotonic()
|
|
104
139
|
if remaining <= 0:
|
|
105
140
|
break
|
|
106
|
-
status, result = post(url + "/v1/auth/browser-login/redeem",
|
|
107
|
-
|
|
141
|
+
status, result, retry_after = post(url + "/v1/auth/browser-login/redeem",
|
|
142
|
+
{"transaction_id": identifier, "poll_secret": proof}, min(timeout, remaining))
|
|
108
143
|
if status == 202 and result == {"status": "pending"}:
|
|
109
144
|
continue
|
|
110
145
|
if status == 429:
|
|
111
|
-
interval = min(30, interval + 2)
|
|
146
|
+
wait = interval = min(30, interval + 2)
|
|
147
|
+
continue
|
|
148
|
+
if status == 503 and retry_after is not None:
|
|
149
|
+
wait = max(interval, retry_after)
|
|
112
150
|
continue
|
|
113
151
|
if status != 200:
|
|
114
152
|
raise rejected()
|
|
@@ -128,7 +166,7 @@ def login(url, timeout, store, reference, *, no_browser=False):
|
|
|
128
166
|
except BaseException:
|
|
129
167
|
try:
|
|
130
168
|
client.revoke_current_session()
|
|
131
|
-
except Exception:
|
|
132
|
-
|
|
169
|
+
except Exception as caught:
|
|
170
|
+
LOGGER.debug("New session revocation failed", extra={"attributes": {"error.type": type(caught).__name__}})
|
|
133
171
|
raise
|
|
134
172
|
return account, session["expires_at"]
|