intflow-authctl 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- intflow_authctl-0.2.3/PKG-INFO +53 -0
- intflow_authctl-0.2.3/README.md +32 -0
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/pyproject.toml +1 -1
- intflow_authctl-0.2.3/src/intflow_authctl/__init__.py +1 -0
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/api_client.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/audit.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/cli.py +1 -3
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/clients.py +77 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/command_utils.py +1 -7
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/commands.py +0 -3
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/config.py +1 -3
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/constants.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/credentials.py +7 -13
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/discovery.py +5 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/errors.py +1 -6
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/external_access.py +1 -5
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/output.py +25 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/parser.py +118 -6
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/secret_output.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/tokens.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/users.py +0 -2
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/validation.py +4 -5
- intflow_authctl-0.2.2/PKG-INFO +0 -136
- intflow_authctl-0.2.2/README.md +0 -115
- intflow_authctl-0.2.2/src/intflow_authctl/__init__.py +0 -3
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/.gitignore +0 -0
- {intflow_authctl-0.2.2 → intflow_authctl-0.2.3}/src/intflow_authctl/py.typed +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intflow-authctl
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Intflow Auth operator CLI.
|
|
5
|
+
Author: Intflow
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: httpx<1,>=0.28
|
|
9
|
+
Requires-Dist: keyring<26,>=25
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
12
|
+
Requires-Dist: hatchling==1.31.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy<3,>=1.13; extra == 'dev'
|
|
14
|
+
Requires-Dist: packaging<27,>=24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-cov<8,>=6; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-timeout<3,>=2.4; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff<1,>=0.9; extra == 'dev'
|
|
19
|
+
Requires-Dist: twine<7,>=6; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# intflow-authctl
|
|
23
|
+
|
|
24
|
+
Intflow Auth 운영자용 CLI의 Python 배포판입니다. 권한을 받은 운영자가 사용자, OAuth client, 운영자 토큰, 감사 기록을 관리할 때 씁니다. 서비스 자체나 데이터베이스 migration은 들어 있지 않습니다.
|
|
25
|
+
|
|
26
|
+
## 요구 사항
|
|
27
|
+
|
|
28
|
+
- Python 3.12 이상
|
|
29
|
+
- Intflow Auth `operator` 또는 `admin` 역할
|
|
30
|
+
|
|
31
|
+
## 설치
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m pip install --user --upgrade intflow-authctl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 사용 예
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
intflow-authctl login
|
|
41
|
+
intflow-authctl whoami
|
|
42
|
+
intflow-authctl clients list
|
|
43
|
+
intflow-authctl clients rotate-secret <oauth-client-id> --secret-file /private/rotation.json
|
|
44
|
+
intflow-authctl logout --revoke
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
로그인한 운영자 신원과 client 목록을 확인합니다. 교체한 client secret은 `--secret-file`로 지정한 보호된 새 파일에만 저장되고, `logout --revoke`는 서버 토큰도 폐기합니다. 운영자 자격증명은 지원되는 OS 자격증명 저장소에 둡니다.
|
|
48
|
+
|
|
49
|
+
## 문서
|
|
50
|
+
|
|
51
|
+
- 명령과 옵션: `intflow-authctl --help`와 `intflow-authctl docs`, 접근 진단은 `intflow-authctl doctor`
|
|
52
|
+
- 상세 문서: 저장소 `yeon3724/intflow-auth`의 `docs/operations.md` (Routine administration, 영어)
|
|
53
|
+
- 저장소: `yeon3724/intflow-auth` (읽기 전용 미러 `intflow/intflow-auth`)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# intflow-authctl
|
|
2
|
+
|
|
3
|
+
Intflow Auth 운영자용 CLI의 Python 배포판입니다. 권한을 받은 운영자가 사용자, OAuth client, 운영자 토큰, 감사 기록을 관리할 때 씁니다. 서비스 자체나 데이터베이스 migration은 들어 있지 않습니다.
|
|
4
|
+
|
|
5
|
+
## 요구 사항
|
|
6
|
+
|
|
7
|
+
- Python 3.12 이상
|
|
8
|
+
- Intflow Auth `operator` 또는 `admin` 역할
|
|
9
|
+
|
|
10
|
+
## 설치
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m pip install --user --upgrade intflow-authctl
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 사용 예
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
intflow-authctl login
|
|
20
|
+
intflow-authctl whoami
|
|
21
|
+
intflow-authctl clients list
|
|
22
|
+
intflow-authctl clients rotate-secret <oauth-client-id> --secret-file /private/rotation.json
|
|
23
|
+
intflow-authctl logout --revoke
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
로그인한 운영자 신원과 client 목록을 확인합니다. 교체한 client secret은 `--secret-file`로 지정한 보호된 새 파일에만 저장되고, `logout --revoke`는 서버 토큰도 폐기합니다. 운영자 자격증명은 지원되는 OS 자격증명 저장소에 둡니다.
|
|
27
|
+
|
|
28
|
+
## 문서
|
|
29
|
+
|
|
30
|
+
- 명령과 옵션: `intflow-authctl --help`와 `intflow-authctl docs`, 접근 진단은 `intflow-authctl doctor`
|
|
31
|
+
- 상세 문서: 저장소 `yeon3724/intflow-auth`의 `docs/operations.md` (Routine administration, 영어)
|
|
32
|
+
- 저장소: `yeon3724/intflow-auth` (읽기 전용 미러 `intflow/intflow-auth`)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Operator Admin API command-line entry point."""
|
|
2
|
-
|
|
3
1
|
import sys
|
|
4
2
|
from typing import TextIO
|
|
5
3
|
|
|
@@ -39,7 +37,7 @@ def authctl_main(
|
|
|
39
37
|
client_factory=httpx.Client,
|
|
40
38
|
credential_store: AuthctlCredentialStore | None = None,
|
|
41
39
|
) -> None:
|
|
42
|
-
|
|
40
|
+
|
|
43
41
|
parser = _build_authctl_parser()
|
|
44
42
|
args = parser.parse_args(argv)
|
|
45
43
|
try:
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""OAuth client command handlers for authctl."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
|
|
5
3
|
from intflow_authctl.api_client import _api_request
|
|
@@ -63,6 +61,8 @@ def _run_authctl_clients_command(
|
|
|
63
61
|
}
|
|
64
62
|
if args.admin_access_required:
|
|
65
63
|
create_body["admin_access_required"] = True
|
|
64
|
+
if args.assignment_required:
|
|
65
|
+
create_body["assignment_required"] = True
|
|
66
66
|
return with_secret_file(
|
|
67
67
|
args.secret_file,
|
|
68
68
|
lambda: _api_request(
|
|
@@ -89,6 +89,10 @@ def _run_authctl_clients_command(
|
|
|
89
89
|
update_body["admin_access_required"] = True
|
|
90
90
|
if args.no_admin_access_required:
|
|
91
91
|
update_body["admin_access_required"] = False
|
|
92
|
+
if args.assignment_required:
|
|
93
|
+
update_body["assignment_required"] = True
|
|
94
|
+
if args.no_assignment_required:
|
|
95
|
+
update_body["assignment_required"] = False
|
|
92
96
|
if not update_body:
|
|
93
97
|
raise AuthctlError("At least one client update field is required.")
|
|
94
98
|
return _api_request(
|
|
@@ -123,6 +127,18 @@ def _run_authctl_clients_command(
|
|
|
123
127
|
config,
|
|
124
128
|
client_factory=client_factory,
|
|
125
129
|
)
|
|
130
|
+
if args.clients_command == "roles":
|
|
131
|
+
return _run_authctl_client_roles_command(
|
|
132
|
+
args,
|
|
133
|
+
config,
|
|
134
|
+
client_factory=client_factory,
|
|
135
|
+
)
|
|
136
|
+
if args.clients_command == "pending-roles":
|
|
137
|
+
return _run_authctl_client_pending_roles_command(
|
|
138
|
+
args,
|
|
139
|
+
config,
|
|
140
|
+
client_factory=client_factory,
|
|
141
|
+
)
|
|
126
142
|
raise AuthctlError(f"Unsupported clients command: {args.clients_command}")
|
|
127
143
|
|
|
128
144
|
|
|
@@ -158,6 +174,65 @@ def _run_authctl_client_admins_command(
|
|
|
158
174
|
raise AuthctlError(f"Unsupported clients admins command: {args.client_admins_command}")
|
|
159
175
|
|
|
160
176
|
|
|
177
|
+
def _run_authctl_client_roles_command(
|
|
178
|
+
args: argparse.Namespace,
|
|
179
|
+
config: dict[str, object],
|
|
180
|
+
*,
|
|
181
|
+
client_factory,
|
|
182
|
+
) -> dict[str, object]:
|
|
183
|
+
base_path = f"/v1/admin/clients/{args.client_id}"
|
|
184
|
+
if args.client_roles_command == "list":
|
|
185
|
+
return _api_request(
|
|
186
|
+
config,
|
|
187
|
+
"GET",
|
|
188
|
+
f"{base_path}/role-assignments",
|
|
189
|
+
params=_request_params(args, "limit", "cursor", extra={"role_name": args.role}),
|
|
190
|
+
client_factory=client_factory,
|
|
191
|
+
)
|
|
192
|
+
if args.client_roles_command in {"grant", "revoke"}:
|
|
193
|
+
return _api_request(
|
|
194
|
+
config,
|
|
195
|
+
"PUT" if args.client_roles_command == "grant" else "DELETE",
|
|
196
|
+
f"{base_path}/roles/{args.role_name}/users/{args.user_id}",
|
|
197
|
+
client_factory=client_factory,
|
|
198
|
+
)
|
|
199
|
+
raise AuthctlError(f"Unsupported clients roles command: {args.client_roles_command}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _run_authctl_client_pending_roles_command(
|
|
203
|
+
args: argparse.Namespace,
|
|
204
|
+
config: dict[str, object],
|
|
205
|
+
*,
|
|
206
|
+
client_factory,
|
|
207
|
+
) -> dict[str, object]:
|
|
208
|
+
base_path = f"/v1/admin/clients/{args.client_id}/pending-role-grants"
|
|
209
|
+
command = args.client_pending_roles_command
|
|
210
|
+
if command == "list":
|
|
211
|
+
return _api_request(
|
|
212
|
+
config,
|
|
213
|
+
"GET",
|
|
214
|
+
base_path,
|
|
215
|
+
params=_request_params(args, "email", "limit", "cursor"),
|
|
216
|
+
client_factory=client_factory,
|
|
217
|
+
)
|
|
218
|
+
if command == "create":
|
|
219
|
+
return _api_request(
|
|
220
|
+
config,
|
|
221
|
+
"POST",
|
|
222
|
+
base_path,
|
|
223
|
+
json_body={"email": args.email, "role_name": args.role_name},
|
|
224
|
+
client_factory=client_factory,
|
|
225
|
+
)
|
|
226
|
+
if command == "delete":
|
|
227
|
+
return _api_request(
|
|
228
|
+
config,
|
|
229
|
+
"DELETE",
|
|
230
|
+
f"{base_path}/{args.grant_id}",
|
|
231
|
+
client_factory=client_factory,
|
|
232
|
+
)
|
|
233
|
+
raise AuthctlError(f"Unsupported clients pending-roles command: {command}")
|
|
234
|
+
|
|
235
|
+
|
|
161
236
|
def _validate_cli_redirect_uris(values: list[str]) -> tuple[str, ...]:
|
|
162
237
|
try:
|
|
163
238
|
return validate_redirect_uris(values)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Shared helpers for authctl command-family modules."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
|
|
5
3
|
from intflow_authctl.output import _response_list
|
|
@@ -10,11 +8,7 @@ def _request_params(
|
|
|
10
8
|
*names: str,
|
|
11
9
|
extra: dict[str, object] | None = None,
|
|
12
10
|
) -> dict[str, object] | None:
|
|
13
|
-
params = {
|
|
14
|
-
name: value
|
|
15
|
-
for name in names
|
|
16
|
-
if (value := getattr(args, name, None)) is not None
|
|
17
|
-
}
|
|
11
|
+
params = {name: value for name in names if (value := getattr(args, name, None)) is not None}
|
|
18
12
|
if extra is not None:
|
|
19
13
|
params.update({name: value for name, value in extra.items() if value is not None})
|
|
20
14
|
return params or None
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Command dispatch for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
import math
|
|
5
3
|
import os
|
|
@@ -266,7 +264,6 @@ def _run_authctl_browser_login(
|
|
|
266
264
|
timeout_seconds=remaining,
|
|
267
265
|
)
|
|
268
266
|
except AuthctlHttpError as exc:
|
|
269
|
-
# Retry only a definite rejection before redemption; never replay a lost response.
|
|
270
267
|
if exc.status != 429:
|
|
271
268
|
raise
|
|
272
269
|
delay = max(poll_interval, exc.retry_after or server_interval)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Credential configuration helpers for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
import json
|
|
5
3
|
import os
|
|
@@ -179,7 +177,7 @@ def _save_authctl_config(
|
|
|
179
177
|
|
|
180
178
|
|
|
181
179
|
def _storage_warning(message: str) -> None:
|
|
182
|
-
|
|
180
|
+
|
|
183
181
|
import sys
|
|
184
182
|
|
|
185
183
|
print(f"WARNING: {message} Remote tokens were not revoked.", file=sys.stderr)
|
|
@@ -1,49 +1,43 @@
|
|
|
1
|
-
"""Credential-store adapters for the intflow-authctl CLI."""
|
|
2
|
-
|
|
3
1
|
from importlib import import_module
|
|
4
2
|
from typing import Protocol
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
class AuthctlCredentialStore(Protocol):
|
|
8
|
-
"""Minimal credential-store interface used by intflow-authctl."""
|
|
9
|
-
|
|
10
6
|
backend_name: str
|
|
11
7
|
|
|
12
8
|
def get_password(self, service: str, account: str) -> str | None:
|
|
13
|
-
|
|
9
|
+
pass
|
|
14
10
|
|
|
15
11
|
def set_password(self, service: str, account: str, password: str) -> None:
|
|
16
|
-
|
|
12
|
+
pass
|
|
17
13
|
|
|
18
14
|
def delete_password(self, service: str, account: str) -> None:
|
|
19
|
-
|
|
15
|
+
pass
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
class AuthctlCredentialStoreError(RuntimeError):
|
|
23
|
-
|
|
19
|
+
pass
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
class KeyringAuthctlCredentialStore:
|
|
27
|
-
"""Credential-store adapter backed by the optional keyring package."""
|
|
28
|
-
|
|
29
23
|
backend_name = "keyring"
|
|
30
24
|
|
|
31
25
|
def get_password(self, service: str, account: str) -> str | None:
|
|
32
|
-
|
|
26
|
+
|
|
33
27
|
try:
|
|
34
28
|
return self._keyring().get_password(service, account)
|
|
35
29
|
except Exception as exc:
|
|
36
30
|
raise AuthctlCredentialStoreError("Credential store lookup failed.") from exc
|
|
37
31
|
|
|
38
32
|
def set_password(self, service: str, account: str, password: str) -> None:
|
|
39
|
-
|
|
33
|
+
|
|
40
34
|
try:
|
|
41
35
|
self._keyring().set_password(service, account, password)
|
|
42
36
|
except Exception as exc:
|
|
43
37
|
raise AuthctlCredentialStoreError("Credential store write failed.") from exc
|
|
44
38
|
|
|
45
39
|
def delete_password(self, service: str, account: str) -> None:
|
|
46
|
-
|
|
40
|
+
|
|
47
41
|
try:
|
|
48
42
|
self._keyring().delete_password(service, account)
|
|
49
43
|
except Exception as exc:
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
def _is_app_role_path(path: str) -> bool:
|
|
2
|
+
return any(
|
|
3
|
+
part in path for part in ("/admins", "/roles/", "/role-assignments", "/pending-role-grants")
|
|
4
|
+
)
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
def permission_hint(status: int, method: str, path: str) -> str:
|
|
@@ -8,7 +11,7 @@ def permission_hint(status: int, method: str, path: str) -> str:
|
|
|
8
11
|
return ""
|
|
9
12
|
resource = None
|
|
10
13
|
if path.startswith("/v1/admin/clients"):
|
|
11
|
-
resource = "app_roles" if
|
|
14
|
+
resource = "app_roles" if _is_app_role_path(path) else "clients"
|
|
12
15
|
elif path.startswith("/v1/admin/operator-tokens"):
|
|
13
16
|
resource = "tokens"
|
|
14
17
|
elif path.startswith("/v1/admin/audit"):
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
"""Error types for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
class AuthctlError(RuntimeError):
|
|
5
|
-
|
|
2
|
+
pass
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
class AuthctlHttpError(AuthctlError):
|
|
9
|
-
"""Structured HTTP metadata without retaining upstream bodies or credentials."""
|
|
10
|
-
|
|
11
6
|
def __init__(self, message: str, status: int, retry_after: int | None) -> None:
|
|
12
7
|
super().__init__(message)
|
|
13
8
|
self.status = status
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Temporary external Google access command handlers."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
from urllib.parse import quote
|
|
5
3
|
|
|
@@ -42,6 +40,4 @@ def _run_authctl_external_access_command(
|
|
|
42
40
|
json_body=body,
|
|
43
41
|
client_factory=client_factory,
|
|
44
42
|
)
|
|
45
|
-
raise AuthctlError(
|
|
46
|
-
f"Unsupported external-access command: {args.external_access_command}"
|
|
47
|
-
)
|
|
43
|
+
raise AuthctlError(f"Unsupported external-access command: {args.external_access_command}")
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Output rendering helpers for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
import json
|
|
5
3
|
from datetime import datetime
|
|
@@ -110,6 +108,31 @@ def _table_shape(
|
|
|
110
108
|
("role_name", "ROLE"),
|
|
111
109
|
("assigned_at", "ASSIGNED_AT"),
|
|
112
110
|
], _response_list(payload, "admins")
|
|
111
|
+
if (
|
|
112
|
+
args.command == "clients"
|
|
113
|
+
and args.clients_command == "roles"
|
|
114
|
+
and args.client_roles_command == "list"
|
|
115
|
+
):
|
|
116
|
+
return [
|
|
117
|
+
("id", "ID"),
|
|
118
|
+
("client_id", "CLIENT_ID"),
|
|
119
|
+
("user_id", "USER_ID"),
|
|
120
|
+
("role_name", "ROLE"),
|
|
121
|
+
("assigned_by_user_id", "ASSIGNED_BY"),
|
|
122
|
+
("assigned_at", "ASSIGNED_AT"),
|
|
123
|
+
], _response_list(payload, "assignments")
|
|
124
|
+
if (
|
|
125
|
+
args.command == "clients"
|
|
126
|
+
and args.clients_command == "pending-roles"
|
|
127
|
+
and args.client_pending_roles_command == "list"
|
|
128
|
+
):
|
|
129
|
+
return [
|
|
130
|
+
("id", "ID"),
|
|
131
|
+
("email", "EMAIL"),
|
|
132
|
+
("role_name", "ROLE"),
|
|
133
|
+
("expires_at", "EXPIRES_AT"),
|
|
134
|
+
("consumed_at", "CONSUMED_AT"),
|
|
135
|
+
], _response_list(payload, "grants")
|
|
113
136
|
if args.command == "audit" and args.audit_command == "list":
|
|
114
137
|
return [
|
|
115
138
|
("id", "ID"),
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Argument parser construction for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
import difflib
|
|
5
3
|
import re
|
|
@@ -12,6 +10,7 @@ from intflow_authctl.constants import (
|
|
|
12
10
|
DEFAULT_AUTHCTL_LOGIN_TTL,
|
|
13
11
|
)
|
|
14
12
|
from intflow_authctl.validation import (
|
|
13
|
+
APP_ROLE_NAMES,
|
|
15
14
|
OAUTH_CLIENT_TYPES,
|
|
16
15
|
OPERATOR_ADMIN_TOKEN_STATUSES,
|
|
17
16
|
ROLE_NAMES,
|
|
@@ -24,8 +23,6 @@ from intflow_authctl.validation import (
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class AuthctlArgumentParser(argparse.ArgumentParser):
|
|
27
|
-
"""ArgumentParser with lightweight command typo suggestions."""
|
|
28
|
-
|
|
29
26
|
def error(self, message: str) -> NoReturn:
|
|
30
27
|
suggestion = _invalid_choice_suggestion(message)
|
|
31
28
|
if suggestion is None:
|
|
@@ -543,7 +540,8 @@ def _build_authctl_parser() -> argparse.ArgumentParser:
|
|
|
543
540
|
' intflow-authctl clients find --name "Console"\n'
|
|
544
541
|
' intflow-authctl clients create --name "Console" --client-type public '
|
|
545
542
|
"--redirect-uri https://app.example/callback --scope openid --scope email\n"
|
|
546
|
-
" intflow-authctl clients admins grant <client_id> <user_id
|
|
543
|
+
" intflow-authctl clients admins grant <client_id> <user_id>\n"
|
|
544
|
+
" intflow-authctl clients roles grant <client_id> lead <user_id>"
|
|
547
545
|
),
|
|
548
546
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
549
547
|
)
|
|
@@ -584,7 +582,8 @@ def _build_authctl_parser() -> argparse.ArgumentParser:
|
|
|
584
582
|
client_create_parser.description = (
|
|
585
583
|
"Use public without a trusted backend; confidential requires secret storage. "
|
|
586
584
|
"Callbacks match exactly; PKCE S256 is required. Use openid email profile; offline_access "
|
|
587
|
-
"also enables refresh grants. admin_access_required
|
|
585
|
+
"also enables refresh grants. admin_access_required and assignment_required default to "
|
|
586
|
+
"false and grant no roles. "
|
|
588
587
|
"Required scope: admin:clients:write. Without --secret-file the secret is printed once."
|
|
589
588
|
)
|
|
590
589
|
client_create_parser.add_argument(
|
|
@@ -619,6 +618,11 @@ def _build_authctl_parser() -> argparse.ArgumentParser:
|
|
|
619
618
|
action="store_true",
|
|
620
619
|
help="Require app-admin assignment before users can authorize this client.",
|
|
621
620
|
)
|
|
621
|
+
client_create_parser.add_argument(
|
|
622
|
+
"--assignment-required",
|
|
623
|
+
action="store_true",
|
|
624
|
+
help="Require any application role assignment before users can authorize this client.",
|
|
625
|
+
)
|
|
622
626
|
client_update_parser = client_subparsers.add_parser("update", help="Update an OAuth client.")
|
|
623
627
|
client_update_parser.add_argument("client_id", help="OAuth client ID to update.")
|
|
624
628
|
client_update_parser.add_argument("--name", help="New OAuth client display name.")
|
|
@@ -648,6 +652,17 @@ def _build_authctl_parser() -> argparse.ArgumentParser:
|
|
|
648
652
|
action="store_true",
|
|
649
653
|
help="Allow normal authorization without app-admin assignment.",
|
|
650
654
|
)
|
|
655
|
+
assignment_group = client_update_parser.add_mutually_exclusive_group()
|
|
656
|
+
assignment_group.add_argument(
|
|
657
|
+
"--assignment-required",
|
|
658
|
+
action="store_true",
|
|
659
|
+
help="Require any application role assignment before users can authorize this client.",
|
|
660
|
+
)
|
|
661
|
+
assignment_group.add_argument(
|
|
662
|
+
"--no-assignment-required",
|
|
663
|
+
action="store_true",
|
|
664
|
+
help="Allow normal authorization without an application role assignment.",
|
|
665
|
+
)
|
|
651
666
|
client_disable_parser = client_subparsers.add_parser(
|
|
652
667
|
"disable",
|
|
653
668
|
help="Disable an OAuth client.",
|
|
@@ -716,6 +731,103 @@ def _build_authctl_parser() -> argparse.ArgumentParser:
|
|
|
716
731
|
)
|
|
717
732
|
client_admins_revoke_parser.add_argument("client_id", help="OAuth client ID.")
|
|
718
733
|
client_admins_revoke_parser.add_argument("user_id", help="Internal users.id to revoke.")
|
|
734
|
+
client_roles_parser = client_subparsers.add_parser(
|
|
735
|
+
"roles",
|
|
736
|
+
help="Manage application role assignments for an OAuth client.",
|
|
737
|
+
description=(
|
|
738
|
+
"Manage application role assignments for one OAuth client.\n\n"
|
|
739
|
+
"The roles are admin, lead and member. The client's access tokens carry "
|
|
740
|
+
"every role a user holds in app_roles. These are not Intflow Auth "
|
|
741
|
+
"management roles.\n\n"
|
|
742
|
+
"Use an internal users.id value for <user_id>. Run "
|
|
743
|
+
"intflow-authctl users list or users get to look up internal user IDs."
|
|
744
|
+
),
|
|
745
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
746
|
+
)
|
|
747
|
+
client_roles_subparsers = client_roles_parser.add_subparsers(
|
|
748
|
+
dest="client_roles_command",
|
|
749
|
+
required=True,
|
|
750
|
+
parser_class=AuthctlArgumentParser,
|
|
751
|
+
)
|
|
752
|
+
client_roles_list_parser = client_roles_subparsers.add_parser(
|
|
753
|
+
"list",
|
|
754
|
+
help="List application role assignments for an OAuth client.",
|
|
755
|
+
)
|
|
756
|
+
client_roles_list_parser.add_argument("client_id", help="OAuth client ID.")
|
|
757
|
+
client_roles_list_parser.add_argument(
|
|
758
|
+
"--role",
|
|
759
|
+
choices=APP_ROLE_NAMES,
|
|
760
|
+
help="Return only assignments of this application role.",
|
|
761
|
+
)
|
|
762
|
+
_add_pagination_arguments(client_roles_list_parser)
|
|
763
|
+
_add_output_format_argument(client_roles_list_parser)
|
|
764
|
+
client_roles_grant_parser = client_roles_subparsers.add_parser(
|
|
765
|
+
"grant",
|
|
766
|
+
help="Grant an application role to an internal user.",
|
|
767
|
+
)
|
|
768
|
+
client_roles_grant_parser.add_argument("client_id", help="OAuth client ID.")
|
|
769
|
+
client_roles_grant_parser.add_argument(
|
|
770
|
+
"role_name",
|
|
771
|
+
choices=APP_ROLE_NAMES,
|
|
772
|
+
help="Application role to grant.",
|
|
773
|
+
)
|
|
774
|
+
client_roles_grant_parser.add_argument("user_id", help="Internal users.id to grant.")
|
|
775
|
+
client_roles_revoke_parser = client_roles_subparsers.add_parser(
|
|
776
|
+
"revoke",
|
|
777
|
+
help="Revoke an application role from an internal user.",
|
|
778
|
+
)
|
|
779
|
+
client_roles_revoke_parser.add_argument("client_id", help="OAuth client ID.")
|
|
780
|
+
client_roles_revoke_parser.add_argument(
|
|
781
|
+
"role_name",
|
|
782
|
+
choices=APP_ROLE_NAMES,
|
|
783
|
+
help="Application role to revoke.",
|
|
784
|
+
)
|
|
785
|
+
client_roles_revoke_parser.add_argument("user_id", help="Internal users.id to revoke.")
|
|
786
|
+
client_pending_roles_parser = client_subparsers.add_parser(
|
|
787
|
+
"pending-roles",
|
|
788
|
+
help="Manage pending application role grants for an OAuth client.",
|
|
789
|
+
description=(
|
|
790
|
+
"Manage pending application role grants for one OAuth client.\n\n"
|
|
791
|
+
"A pending grant names a company e-mail address that has not signed in yet. "
|
|
792
|
+
"The first sign-in of the verified intflow.ai account with that address "
|
|
793
|
+
"turns it into an application role assignment once. A grant expires after "
|
|
794
|
+
"90 days. These are not Intflow Auth management roles."
|
|
795
|
+
),
|
|
796
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
797
|
+
)
|
|
798
|
+
client_pending_roles_subparsers = client_pending_roles_parser.add_subparsers(
|
|
799
|
+
dest="client_pending_roles_command",
|
|
800
|
+
required=True,
|
|
801
|
+
parser_class=AuthctlArgumentParser,
|
|
802
|
+
)
|
|
803
|
+
client_pending_roles_list_parser = client_pending_roles_subparsers.add_parser(
|
|
804
|
+
"list",
|
|
805
|
+
help="List pending application role grants for an OAuth client.",
|
|
806
|
+
)
|
|
807
|
+
client_pending_roles_list_parser.add_argument("client_id", help="OAuth client ID.")
|
|
808
|
+
client_pending_roles_list_parser.add_argument(
|
|
809
|
+
"--email",
|
|
810
|
+
help="Return only the grants of this company e-mail address.",
|
|
811
|
+
)
|
|
812
|
+
_add_pagination_arguments(client_pending_roles_list_parser)
|
|
813
|
+
_add_output_format_argument(client_pending_roles_list_parser)
|
|
814
|
+
client_pending_roles_create_parser = client_pending_roles_subparsers.add_parser(
|
|
815
|
+
"create",
|
|
816
|
+
help="Create a pending application role grant for a company e-mail address.",
|
|
817
|
+
)
|
|
818
|
+
client_pending_roles_create_parser.add_argument("client_id", help="OAuth client ID.")
|
|
819
|
+
client_pending_roles_create_parser.add_argument(
|
|
820
|
+
"role_name",
|
|
821
|
+
choices=APP_ROLE_NAMES,
|
|
822
|
+
help="Application role to grant at the first sign-in.",
|
|
823
|
+
)
|
|
824
|
+
client_pending_roles_create_parser.add_argument("email", help="Company e-mail address.")
|
|
825
|
+
client_pending_roles_delete_parser = client_pending_roles_subparsers.add_parser(
|
|
826
|
+
"delete",
|
|
827
|
+
help="Delete a pending application role grant.",
|
|
828
|
+
)
|
|
829
|
+
client_pending_roles_delete_parser.add_argument("client_id", help="OAuth client ID.")
|
|
830
|
+
client_pending_roles_delete_parser.add_argument("grant_id", help="Pending grant ID.")
|
|
719
831
|
|
|
720
832
|
external_access_parser = subparsers.add_parser(
|
|
721
833
|
"external-access",
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Validation helpers for the Intflow Auth operator CLI."""
|
|
2
|
-
|
|
3
1
|
import argparse
|
|
4
2
|
import ipaddress
|
|
5
3
|
import math
|
|
@@ -13,6 +11,7 @@ OAUTH_GRANT_TYPES = ("authorization_code", "refresh_token")
|
|
|
13
11
|
OAUTH_SCOPES = ("openid", "email", "profile", "offline_access")
|
|
14
12
|
OPERATOR_ADMIN_TOKEN_STATUSES = ("active", "revoked", "expired")
|
|
15
13
|
ROLE_NAMES = ("user", "operator", "admin")
|
|
14
|
+
APP_ROLE_NAMES = ("admin", "lead", "member")
|
|
16
15
|
USER_STATUSES = ("active", "suspended")
|
|
17
16
|
HTTP_LOOPBACK_REDIRECT_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
|
|
18
17
|
ADMIN_BASE_URL_LOOPBACK_HOSTS = HTTP_LOOPBACK_REDIRECT_HOSTS
|
|
@@ -21,7 +20,7 @@ OAUTH_SCOPE_SET = frozenset(OAUTH_SCOPES)
|
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class OAuthClientValidationError(ValueError):
|
|
24
|
-
|
|
23
|
+
pass
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def _positive_float(value: float, name: str) -> float:
|
|
@@ -65,7 +64,7 @@ def _non_empty_search_text(value: str) -> str:
|
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
def validate_redirect_uris(values: list[str] | tuple[str, ...]) -> tuple[str, ...]:
|
|
68
|
-
|
|
67
|
+
|
|
69
68
|
validated: list[str] = []
|
|
70
69
|
seen: set[str] = set()
|
|
71
70
|
for value in values:
|
|
@@ -80,7 +79,7 @@ def validate_redirect_uris(values: list[str] | tuple[str, ...]) -> tuple[str, ..
|
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
def validate_oauth_scopes(values: list[str] | tuple[str, ...]) -> tuple[str, ...]:
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
validated: list[str] = []
|
|
85
84
|
seen: set[str] = set()
|
|
86
85
|
for value in values:
|
intflow_authctl-0.2.2/PKG-INFO
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: intflow-authctl
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Intflow Auth operator CLI.
|
|
5
|
-
Author: Intflow
|
|
6
|
-
License: Proprietary
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Requires-Dist: httpx<1,>=0.28
|
|
9
|
-
Requires-Dist: keyring<26,>=25
|
|
10
|
-
Provides-Extra: dev
|
|
11
|
-
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
12
|
-
Requires-Dist: hatchling==1.31.0; extra == 'dev'
|
|
13
|
-
Requires-Dist: mypy<3,>=1.13; extra == 'dev'
|
|
14
|
-
Requires-Dist: packaging<27,>=24; extra == 'dev'
|
|
15
|
-
Requires-Dist: pytest-cov<8,>=6; extra == 'dev'
|
|
16
|
-
Requires-Dist: pytest-timeout<3,>=2.4; extra == 'dev'
|
|
17
|
-
Requires-Dist: pytest<10,>=8; extra == 'dev'
|
|
18
|
-
Requires-Dist: ruff<1,>=0.9; extra == 'dev'
|
|
19
|
-
Requires-Dist: twine<7,>=6; extra == 'dev'
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# intflow-authctl
|
|
23
|
-
|
|
24
|
-
`intflow-authctl` is the Python operator CLI package for Intflow Auth.
|
|
25
|
-
|
|
26
|
-
## Install
|
|
27
|
-
|
|
28
|
-
```powershell
|
|
29
|
-
py -3.12 -m pip install --user --upgrade intflow-authctl
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## CLI
|
|
33
|
-
|
|
34
|
-
```powershell
|
|
35
|
-
intflow-authctl login
|
|
36
|
-
intflow-authctl whoami
|
|
37
|
-
intflow-authctl users list
|
|
38
|
-
intflow-authctl tokens create-no-expiry
|
|
39
|
-
intflow-authctl clients list
|
|
40
|
-
intflow-authctl clients rotate-secret <oauth-client-id> --grace-seconds 0
|
|
41
|
-
intflow-authctl logout --revoke
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The CLI is intended for authorized Intflow operators. It manages local operator
|
|
45
|
-
credentials through the supported credential backends and communicates with an
|
|
46
|
-
Intflow Auth deployment over the Admin API.
|
|
47
|
-
|
|
48
|
-
After CLI login, an `admin`-role user can run `tokens create-no-expiry` to
|
|
49
|
-
verify the current CLI identity and open the browser-only issuance page on the
|
|
50
|
-
same Auth service. Use `--no-browser` to print the URL only. The CLI bearer token
|
|
51
|
-
is never added to the URL or passed to the browser.
|
|
52
|
-
|
|
53
|
-
Failed commands return a nonzero exit code and write a bounded diagnostic to
|
|
54
|
-
stderr. Admin HTTP failures include only the status, a validated service error
|
|
55
|
-
code when present, and bounded `Retry-After` guidance. Upstream descriptions,
|
|
56
|
-
malformed bodies, credentials, and raw transport causes are not rendered.
|
|
57
|
-
Automation should parse JSON stdout only after a successful command.
|
|
58
|
-
|
|
59
|
-
## Package Scope
|
|
60
|
-
|
|
61
|
-
This distribution contains only the operator CLI, Admin API client, output and
|
|
62
|
-
validation logic, and credential-store adapters. It does not contain the Auth
|
|
63
|
-
service, database models, migrations, or service bootstrap commands.
|
|
64
|
-
|
|
65
|
-
## Requirements
|
|
66
|
-
|
|
67
|
-
- Python 3.12 or later.
|
|
68
|
-
- Access to an authorized Intflow Auth deployment.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## Discover commands and diagnose access
|
|
72
|
-
|
|
73
|
-
No repository checkout is needed to explore the installed CLI:
|
|
74
|
-
|
|
75
|
-
```text
|
|
76
|
-
intflow-authctl --help
|
|
77
|
-
intflow-authctl docs
|
|
78
|
-
intflow-authctl login
|
|
79
|
-
intflow-authctl doctor
|
|
80
|
-
intflow-authctl clients create --help
|
|
81
|
-
intflow-authctl users find --help
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Every command has `--help`, including nested commands. `docs` works offline and
|
|
85
|
-
prints onboarding guidance plus links to the API documentation and OpenAPI.
|
|
86
|
-
`docs --open` opens the documentation only when requested; use `--base-url` for
|
|
87
|
-
a different Auth deployment. `doctor` makes one read-only `whoami` request and
|
|
88
|
-
shows the current identity, effective scopes and token metadata without printing
|
|
89
|
-
the bearer token. Missing/expired credentials return exit code 2 with login guidance.
|
|
90
|
-
|
|
91
|
-
A company account does not automatically grant Auth management access. A 403
|
|
92
|
-
explains the required token scope where known, but server roles and target
|
|
93
|
-
restrictions still apply. When using `login --scope`, repeat it for **all** needed
|
|
94
|
-
scopes; it replaces the default scope selection. `logout --revoke` requires
|
|
95
|
-
`admin:tokens:write`; if remote revocation fails, local credentials remain.
|
|
96
|
-
|
|
97
|
-
## Save a client secret without printing it
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
intflow-authctl clients create --name "Internal app" --client-type confidential --redirect-uri https://app.example/callback --scope openid --scope email --scope profile --secret-file /private/new-client.json
|
|
101
|
-
intflow-authctl clients rotate-secret CLIENT_ID --grace-seconds 60 --secret-file /private/new-rotation.json
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Choose a new file in a private existing directory outside the repository. The
|
|
105
|
-
file contains the complete response JSON, including `client_secret`; stdout
|
|
106
|
-
contains metadata and `client_secret_file` instead. The CLI refuses to overwrite
|
|
107
|
-
an existing file and protects the empty file **before** contacting the server
|
|
108
|
-
(POSIX 0600; Windows current-user DACL with inherited access removed).
|
|
109
|
-
If storage fails after the server changes the client, inspect the client and
|
|
110
|
-
rotate to a working destination; do not repeat creation. Without `--secret-file`,
|
|
111
|
-
the existing one-time secret JSON output remains unchanged. Transfer the file
|
|
112
|
-
through your approved secret delivery mechanism and remove it when no longer needed.
|
|
113
|
-
|
|
114
|
-
Public clients cannot hold secrets. Use confidential clients for trusted backends,
|
|
115
|
-
register exact callback URLs, and use PKCE S256. Add `offline_access` only when
|
|
116
|
-
refresh tokens are needed. App administrator assignment is a separate operation.
|
|
117
|
-
|
|
118
|
-
## Recovery and local diagnostics
|
|
119
|
-
|
|
120
|
-
`intflow-authctl --version` identifies the installed version and distribution.
|
|
121
|
-
`doctor` remains read-only; `doctor --check-store` explicitly tests local storage
|
|
122
|
-
with a temporary nonsecret write/read/delete probe and needs no login or server.
|
|
123
|
-
Login runs the same preflight before requesting browser approval.
|
|
124
|
-
|
|
125
|
-
Login polling honors server intervals and bounded HTTP 429 Retry-After guidance.
|
|
126
|
-
It does not retry transport failures or 5xx responses, because one-time redemption
|
|
127
|
-
may already have succeeded. Re-login never revokes server tokens. It removes only
|
|
128
|
-
a previous local entry owned by the same config; legacy/shared entries are retained
|
|
129
|
-
with a warning to preserve other profiles. Local logout can clear a config whose
|
|
130
|
-
keyring entry is already missing. Use `logout --revoke` for explicit remote revocation.
|
|
131
|
-
|
|
132
|
-
`tokens create --name automation --ttl 1h --secret-file /private/new-token.json`
|
|
133
|
-
stores the full one-time response in a new protected file and returns `token_file`
|
|
134
|
-
instead of the bearer. Without that option, existing one-time JSON stdout is unchanged.
|
|
135
|
-
If issuance succeeds but storage fails, inspect and explicitly revoke the unused
|
|
136
|
-
token before repeating issuance. File output uses the same protections as client secrets.
|
intflow_authctl-0.2.2/README.md
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# intflow-authctl
|
|
2
|
-
|
|
3
|
-
`intflow-authctl` is the Python operator CLI package for Intflow Auth.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```powershell
|
|
8
|
-
py -3.12 -m pip install --user --upgrade intflow-authctl
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## CLI
|
|
12
|
-
|
|
13
|
-
```powershell
|
|
14
|
-
intflow-authctl login
|
|
15
|
-
intflow-authctl whoami
|
|
16
|
-
intflow-authctl users list
|
|
17
|
-
intflow-authctl tokens create-no-expiry
|
|
18
|
-
intflow-authctl clients list
|
|
19
|
-
intflow-authctl clients rotate-secret <oauth-client-id> --grace-seconds 0
|
|
20
|
-
intflow-authctl logout --revoke
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
The CLI is intended for authorized Intflow operators. It manages local operator
|
|
24
|
-
credentials through the supported credential backends and communicates with an
|
|
25
|
-
Intflow Auth deployment over the Admin API.
|
|
26
|
-
|
|
27
|
-
After CLI login, an `admin`-role user can run `tokens create-no-expiry` to
|
|
28
|
-
verify the current CLI identity and open the browser-only issuance page on the
|
|
29
|
-
same Auth service. Use `--no-browser` to print the URL only. The CLI bearer token
|
|
30
|
-
is never added to the URL or passed to the browser.
|
|
31
|
-
|
|
32
|
-
Failed commands return a nonzero exit code and write a bounded diagnostic to
|
|
33
|
-
stderr. Admin HTTP failures include only the status, a validated service error
|
|
34
|
-
code when present, and bounded `Retry-After` guidance. Upstream descriptions,
|
|
35
|
-
malformed bodies, credentials, and raw transport causes are not rendered.
|
|
36
|
-
Automation should parse JSON stdout only after a successful command.
|
|
37
|
-
|
|
38
|
-
## Package Scope
|
|
39
|
-
|
|
40
|
-
This distribution contains only the operator CLI, Admin API client, output and
|
|
41
|
-
validation logic, and credential-store adapters. It does not contain the Auth
|
|
42
|
-
service, database models, migrations, or service bootstrap commands.
|
|
43
|
-
|
|
44
|
-
## Requirements
|
|
45
|
-
|
|
46
|
-
- Python 3.12 or later.
|
|
47
|
-
- Access to an authorized Intflow Auth deployment.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## Discover commands and diagnose access
|
|
51
|
-
|
|
52
|
-
No repository checkout is needed to explore the installed CLI:
|
|
53
|
-
|
|
54
|
-
```text
|
|
55
|
-
intflow-authctl --help
|
|
56
|
-
intflow-authctl docs
|
|
57
|
-
intflow-authctl login
|
|
58
|
-
intflow-authctl doctor
|
|
59
|
-
intflow-authctl clients create --help
|
|
60
|
-
intflow-authctl users find --help
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Every command has `--help`, including nested commands. `docs` works offline and
|
|
64
|
-
prints onboarding guidance plus links to the API documentation and OpenAPI.
|
|
65
|
-
`docs --open` opens the documentation only when requested; use `--base-url` for
|
|
66
|
-
a different Auth deployment. `doctor` makes one read-only `whoami` request and
|
|
67
|
-
shows the current identity, effective scopes and token metadata without printing
|
|
68
|
-
the bearer token. Missing/expired credentials return exit code 2 with login guidance.
|
|
69
|
-
|
|
70
|
-
A company account does not automatically grant Auth management access. A 403
|
|
71
|
-
explains the required token scope where known, but server roles and target
|
|
72
|
-
restrictions still apply. When using `login --scope`, repeat it for **all** needed
|
|
73
|
-
scopes; it replaces the default scope selection. `logout --revoke` requires
|
|
74
|
-
`admin:tokens:write`; if remote revocation fails, local credentials remain.
|
|
75
|
-
|
|
76
|
-
## Save a client secret without printing it
|
|
77
|
-
|
|
78
|
-
```text
|
|
79
|
-
intflow-authctl clients create --name "Internal app" --client-type confidential --redirect-uri https://app.example/callback --scope openid --scope email --scope profile --secret-file /private/new-client.json
|
|
80
|
-
intflow-authctl clients rotate-secret CLIENT_ID --grace-seconds 60 --secret-file /private/new-rotation.json
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Choose a new file in a private existing directory outside the repository. The
|
|
84
|
-
file contains the complete response JSON, including `client_secret`; stdout
|
|
85
|
-
contains metadata and `client_secret_file` instead. The CLI refuses to overwrite
|
|
86
|
-
an existing file and protects the empty file **before** contacting the server
|
|
87
|
-
(POSIX 0600; Windows current-user DACL with inherited access removed).
|
|
88
|
-
If storage fails after the server changes the client, inspect the client and
|
|
89
|
-
rotate to a working destination; do not repeat creation. Without `--secret-file`,
|
|
90
|
-
the existing one-time secret JSON output remains unchanged. Transfer the file
|
|
91
|
-
through your approved secret delivery mechanism and remove it when no longer needed.
|
|
92
|
-
|
|
93
|
-
Public clients cannot hold secrets. Use confidential clients for trusted backends,
|
|
94
|
-
register exact callback URLs, and use PKCE S256. Add `offline_access` only when
|
|
95
|
-
refresh tokens are needed. App administrator assignment is a separate operation.
|
|
96
|
-
|
|
97
|
-
## Recovery and local diagnostics
|
|
98
|
-
|
|
99
|
-
`intflow-authctl --version` identifies the installed version and distribution.
|
|
100
|
-
`doctor` remains read-only; `doctor --check-store` explicitly tests local storage
|
|
101
|
-
with a temporary nonsecret write/read/delete probe and needs no login or server.
|
|
102
|
-
Login runs the same preflight before requesting browser approval.
|
|
103
|
-
|
|
104
|
-
Login polling honors server intervals and bounded HTTP 429 Retry-After guidance.
|
|
105
|
-
It does not retry transport failures or 5xx responses, because one-time redemption
|
|
106
|
-
may already have succeeded. Re-login never revokes server tokens. It removes only
|
|
107
|
-
a previous local entry owned by the same config; legacy/shared entries are retained
|
|
108
|
-
with a warning to preserve other profiles. Local logout can clear a config whose
|
|
109
|
-
keyring entry is already missing. Use `logout --revoke` for explicit remote revocation.
|
|
110
|
-
|
|
111
|
-
`tokens create --name automation --ttl 1h --secret-file /private/new-token.json`
|
|
112
|
-
stores the full one-time response in a new protected file and returns `token_file`
|
|
113
|
-
instead of the bearer. Without that option, existing one-time JSON stdout is unchanged.
|
|
114
|
-
If issuance succeeds but storage fails, inspect and explicitly revoke the unused
|
|
115
|
-
token before repeating issuance. File output uses the same protections as client secrets.
|
|
File without changes
|
|
File without changes
|