urun-cli 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {urun_cli-0.2.0 → urun_cli-0.3.0}/CHANGELOG.md +10 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/PKG-INFO +1 -1
- {urun_cli-0.2.0 → urun_cli-0.3.0}/pyproject.toml +1 -1
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/api.py +9 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/cli.py +148 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/.gitignore +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/LICENSE +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/README.md +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/SECURITY.md +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/__init__.py +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/config.py +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/deps.py +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/discovery.py +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/errors.py +0 -0
- {urun_cli-0.2.0 → urun_cli-0.3.0}/src/urun/manifest.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Implement the `urun org` / `urun org id` command: print the caller's org id
|
|
6
|
+
(resolved via the control-plane org-config endpoint).
|
|
7
|
+
- Implement `urun auth jwks set`: register the org's trusted JWKS for federated
|
|
8
|
+
identity, via `--jwks-url` or `--jwks-json` (file or stdin), with optional
|
|
9
|
+
`--issuer`/`--audience`. This registers a trust relationship only; JWTs are
|
|
10
|
+
issued out of band and the CLI never mints, fetches, or stores one.
|
|
11
|
+
- Add `ApiClient.register_trusted_jwks`, calling `POST /org-config/trusted-jwks`.
|
|
12
|
+
|
|
3
13
|
## 0.2.0
|
|
4
14
|
|
|
5
15
|
- Bumped past 0.1.1/0.1.2 because both filenames were occupied by previously-deleted PyPI artifacts.
|
|
@@ -35,6 +35,15 @@ class ApiClient:
|
|
|
35
35
|
def org_config(self) -> dict[str, Any]:
|
|
36
36
|
return self._json("GET", "/org-config", None)
|
|
37
37
|
|
|
38
|
+
def register_trusted_jwks(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
"""Register this org's trusted JWKS with the control plane.
|
|
40
|
+
|
|
41
|
+
The org is derived server-side from the API key; the payload carries only
|
|
42
|
+
the trusted key source (jwks_url OR jwks_json) plus optional iss/aud. This
|
|
43
|
+
records a trust relationship — it does not mint or fetch a JWT.
|
|
44
|
+
"""
|
|
45
|
+
return self._json("POST", "/org-config/trusted-jwks", payload)
|
|
46
|
+
|
|
38
47
|
def _json(self, method: str, path: str, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
39
48
|
data = (
|
|
40
49
|
None
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import getpass
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
@@ -28,6 +29,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
28
29
|
return deploy(args)
|
|
29
30
|
if args.command == "login":
|
|
30
31
|
return login(args)
|
|
32
|
+
if args.command == "org":
|
|
33
|
+
return org(args)
|
|
34
|
+
if args.command == "auth":
|
|
35
|
+
return auth(args)
|
|
31
36
|
parser.print_help()
|
|
32
37
|
return 2
|
|
33
38
|
except UrunError as exc:
|
|
@@ -65,9 +70,81 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
65
70
|
),
|
|
66
71
|
)
|
|
67
72
|
add_deploy_args(run)
|
|
73
|
+
|
|
74
|
+
add_org_parser(sub)
|
|
75
|
+
add_auth_parser(sub)
|
|
68
76
|
return parser
|
|
69
77
|
|
|
70
78
|
|
|
79
|
+
def add_org_parser(sub: argparse._SubParsersAction) -> None:
|
|
80
|
+
org_parser = sub.add_parser(
|
|
81
|
+
"org",
|
|
82
|
+
help="Show the caller's org id",
|
|
83
|
+
description=(
|
|
84
|
+
"Print the organization the authenticating API key belongs to "
|
|
85
|
+
"(resolved via the control plane's org-config endpoint)."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
org_sub = org_parser.add_subparsers(dest="org_command")
|
|
89
|
+
id_parser = org_sub.add_parser(
|
|
90
|
+
"id",
|
|
91
|
+
help="Print just the org id (machine-readable)",
|
|
92
|
+
description="Print only the org id on stdout, with no other output.",
|
|
93
|
+
)
|
|
94
|
+
add_api_args(id_parser)
|
|
95
|
+
# Allow `urun org` (bare) to behave like `urun org id`.
|
|
96
|
+
add_api_args(org_parser)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def add_auth_parser(sub: argparse._SubParsersAction) -> None:
|
|
100
|
+
auth_parser = sub.add_parser(
|
|
101
|
+
"auth",
|
|
102
|
+
help="Register the org's trusted JWKS for federated identity",
|
|
103
|
+
description=(
|
|
104
|
+
"Register the trusted JWKS the control plane should verify session "
|
|
105
|
+
"JWTs against for this org. JWTs are issued OUT OF BAND by your IdP; "
|
|
106
|
+
"this command only REGISTERS A TRUST RELATIONSHIP — it never mints, "
|
|
107
|
+
"fetches, or stores a JWT, and no new secret is sent."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
auth_sub = auth_parser.add_subparsers(dest="auth_command")
|
|
111
|
+
jwks_parser = auth_sub.add_parser(
|
|
112
|
+
"jwks",
|
|
113
|
+
help="Manage the org's trusted JWKS",
|
|
114
|
+
description="Manage the trusted JWKS registered for this org.",
|
|
115
|
+
)
|
|
116
|
+
jwks_sub = jwks_parser.add_subparsers(dest="jwks_command")
|
|
117
|
+
set_parser = jwks_sub.add_parser(
|
|
118
|
+
"set",
|
|
119
|
+
help="Register/replace the org's trusted JWKS",
|
|
120
|
+
description=(
|
|
121
|
+
"Register (or replace) the trusted key source the control plane "
|
|
122
|
+
"verifies this org's session JWTs against. Provide EXACTLY ONE of "
|
|
123
|
+
"--jwks-url (a remote JWK Set endpoint) or --jwks-json (an inline JWK "
|
|
124
|
+
"Set from a file or '-' for stdin). Optionally pin the expected token "
|
|
125
|
+
"issuer/audience. This registers trust only; it does not mint a JWT."
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
set_parser.add_argument(
|
|
129
|
+
"--jwks-url",
|
|
130
|
+
help="HTTPS URL of the org's JWK Set (e.g. https://idp.example.com/.well-known/jwks.json)",
|
|
131
|
+
)
|
|
132
|
+
set_parser.add_argument(
|
|
133
|
+
"--jwks-json",
|
|
134
|
+
metavar="FILE",
|
|
135
|
+
help="Path to an inline JWK Set ({\"keys\":[...]}), or '-' to read from stdin",
|
|
136
|
+
)
|
|
137
|
+
set_parser.add_argument(
|
|
138
|
+
"--issuer",
|
|
139
|
+
help="Optional expected JWT `iss` claim; tokens whose iss differs are rejected",
|
|
140
|
+
)
|
|
141
|
+
set_parser.add_argument(
|
|
142
|
+
"--audience",
|
|
143
|
+
help="Optional expected JWT `aud` claim; tokens lacking it are rejected",
|
|
144
|
+
)
|
|
145
|
+
add_api_args(set_parser)
|
|
146
|
+
|
|
147
|
+
|
|
71
148
|
def add_login_args(parser: argparse.ArgumentParser) -> None:
|
|
72
149
|
parser.add_argument(
|
|
73
150
|
"--api-url",
|
|
@@ -132,6 +209,77 @@ def login(args: argparse.Namespace) -> int:
|
|
|
132
209
|
return 0
|
|
133
210
|
|
|
134
211
|
|
|
212
|
+
def org(args: argparse.Namespace) -> int:
|
|
213
|
+
# `urun org` and `urun org id` both print just the org id. There is no other
|
|
214
|
+
# `org` subcommand, so a bare `org` defaults to `id`.
|
|
215
|
+
api_url, api_key = resolve_api_credentials(args)
|
|
216
|
+
config = ApiClient(api_url, api_key).org_config()
|
|
217
|
+
print(config_value(config, "org_id"))
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def auth(args: argparse.Namespace) -> int:
|
|
222
|
+
if (
|
|
223
|
+
getattr(args, "auth_command", None) != "jwks"
|
|
224
|
+
or getattr(args, "jwks_command", None) != "set"
|
|
225
|
+
):
|
|
226
|
+
raise UrunError("usage: urun auth jwks set --jwks-url <url> | --jwks-json <file|->")
|
|
227
|
+
return auth_jwks_set(args)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def auth_jwks_set(args: argparse.Namespace) -> int:
|
|
231
|
+
jwks_url = string_value(args.jwks_url)
|
|
232
|
+
jwks_json_arg = string_value(args.jwks_json)
|
|
233
|
+
if bool(jwks_url) == bool(jwks_json_arg):
|
|
234
|
+
raise UrunError("provide exactly one of --jwks-url or --jwks-json")
|
|
235
|
+
|
|
236
|
+
payload: dict[str, Any] = {}
|
|
237
|
+
if jwks_url:
|
|
238
|
+
payload["jwks_url"] = jwks_url
|
|
239
|
+
issuer = string_value(args.issuer)
|
|
240
|
+
audience = string_value(args.audience)
|
|
241
|
+
if issuer:
|
|
242
|
+
payload["expected_issuer"] = issuer
|
|
243
|
+
if audience:
|
|
244
|
+
payload["expected_audience"] = audience
|
|
245
|
+
else:
|
|
246
|
+
payload["jwks_json"] = read_jwks_json(jwks_json_arg)
|
|
247
|
+
issuer = string_value(args.issuer)
|
|
248
|
+
audience = string_value(args.audience)
|
|
249
|
+
if issuer:
|
|
250
|
+
payload["expected_issuer"] = issuer
|
|
251
|
+
if audience:
|
|
252
|
+
payload["expected_audience"] = audience
|
|
253
|
+
|
|
254
|
+
api_url, api_key = resolve_api_credentials(args)
|
|
255
|
+
result = ApiClient(api_url, api_key).register_trusted_jwks(payload)
|
|
256
|
+
org_id = string_value(result.get("org_id")) or "(unknown)"
|
|
257
|
+
print(f"Registered trusted JWKS for org {org_id}.")
|
|
258
|
+
print("This registers a trust relationship only; no JWT was minted or fetched.")
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def read_jwks_json(source: str) -> dict[str, Any]:
|
|
263
|
+
"""Read and validate a JWK Set from a file path, or '-' for stdin."""
|
|
264
|
+
if source == "-":
|
|
265
|
+
raw = sys.stdin.read()
|
|
266
|
+
else:
|
|
267
|
+
path = Path(source)
|
|
268
|
+
if not path.is_file():
|
|
269
|
+
raise UrunError(f"JWKS file not found: {source}")
|
|
270
|
+
raw = path.read_text(encoding="utf-8")
|
|
271
|
+
try:
|
|
272
|
+
parsed = json.loads(raw)
|
|
273
|
+
except json.JSONDecodeError as exc:
|
|
274
|
+
raise UrunError(f"invalid JWK Set JSON: {exc}") from exc
|
|
275
|
+
if not isinstance(parsed, dict):
|
|
276
|
+
raise UrunError("invalid JWK Set: expected a JSON object")
|
|
277
|
+
keys = parsed.get("keys")
|
|
278
|
+
if not isinstance(keys, list) or not keys:
|
|
279
|
+
raise UrunError('invalid JWK Set: must contain a non-empty "keys" array')
|
|
280
|
+
return parsed
|
|
281
|
+
|
|
282
|
+
|
|
135
283
|
def deploy(args: argparse.Namespace) -> int:
|
|
136
284
|
api_url, api_key = resolve_api_credentials(args)
|
|
137
285
|
if args.poll_interval <= 0:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|