boxd 0.2.0.dev40__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.
- {boxd-0.2.0.dev40/src/boxd.egg-info → boxd-0.2.1}/PKG-INFO +1 -1
- {boxd-0.2.0.dev40 → boxd-0.2.1}/pyproject.toml +1 -1
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_credentials.py +15 -16
- {boxd-0.2.0.dev40 → boxd-0.2.1/src/boxd.egg-info}/PKG-INFO +1 -1
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_credentials.py +16 -36
- {boxd-0.2.0.dev40 → boxd-0.2.1}/LICENSE +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/README.md +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/setup.cfg +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/__init__.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_client.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_mappers.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_requests.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_transport.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_urls.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/errors.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/models.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/machines.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_mappers.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_namespaces.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_requests.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_streaming.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_transport.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_urls.py +0 -0
- {boxd-0.2.0.dev40 → boxd-0.2.1}/tests/test_version_check.py +0 -0
|
@@ -35,7 +35,6 @@ METADATA_TOKEN_PATH = "/v1/auth/token"
|
|
|
35
35
|
# Proxy-minted, HMAC-signed identity token exposed to every guest process by
|
|
36
36
|
# takeoff. Forwarded verbatim so the worker attributes the call to the acting
|
|
37
37
|
# user rather than the VM owner.
|
|
38
|
-
IDENTITY_HEADER = "X-Boxd-Identity"
|
|
39
38
|
|
|
40
39
|
# Re-exchange an API-key JWT this long before it expires.
|
|
41
40
|
EXCHANGE_SKEW_SECS = 300
|
|
@@ -78,16 +77,19 @@ def detect_gateway() -> str | None:
|
|
|
78
77
|
return None
|
|
79
78
|
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
# Only a hint: it skips the probe in the common case of running on a laptop.
|
|
81
|
+
# Deliberately permissive — which of these a shell actually has depends on how
|
|
82
|
+
# the session was opened and on how old the machine is, so gating on any single
|
|
83
|
+
# one silently disables in-VM auth on machines that are otherwise fine. The
|
|
84
|
+
# metadata probe is the real test: nothing outside a boxd machine answers it.
|
|
85
|
+
_VM_HINTS = ("BOXD_VM_ID", "BOXD_VM_NAME", "BOXD_PROXY_ENDPOINT")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def looks_like_a_machine() -> bool:
|
|
89
|
+
"""True when the environment hints we may be inside a boxd machine."""
|
|
90
|
+
return any(os.environ.get(v) for v in _VM_HINTS)
|
|
85
91
|
|
|
86
92
|
|
|
87
|
-
def identity_token() -> str | None:
|
|
88
|
-
"""`BOXD_IDENTITY`, the session's signed identity token (already base64)."""
|
|
89
|
-
value = os.environ.get("BOXD_IDENTITY")
|
|
90
|
-
return value or None
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def probe(host: str, port: int, timeout: float = _PROBE_TIMEOUT_SECS) -> bool:
|
|
@@ -185,9 +187,6 @@ class _CredentialsBase:
|
|
|
185
187
|
return "exchange"
|
|
186
188
|
return "auto"
|
|
187
189
|
|
|
188
|
-
def _headers(self) -> dict[str, str]:
|
|
189
|
-
identity = identity_token()
|
|
190
|
-
return {IDENTITY_HEADER: identity} if identity else {}
|
|
191
190
|
|
|
192
191
|
|
|
193
192
|
class SyncCredentials(_CredentialsBase):
|
|
@@ -222,7 +221,7 @@ class SyncCredentials(_CredentialsBase):
|
|
|
222
221
|
base = self._metadata_base()
|
|
223
222
|
try:
|
|
224
223
|
with httpx.Client(timeout=_HTTP_TIMEOUT_SECS) as client:
|
|
225
|
-
resp = client.get(base + METADATA_TOKEN_PATH
|
|
224
|
+
resp = client.get(base + METADATA_TOKEN_PATH)
|
|
226
225
|
except httpx.HTTPError as e:
|
|
227
226
|
raise AuthenticationError(f"in-VM token request failed: {e}") from e
|
|
228
227
|
token, expires_at, api_url = parse_vm_token(
|
|
@@ -234,7 +233,7 @@ class SyncCredentials(_CredentialsBase):
|
|
|
234
233
|
|
|
235
234
|
def _metadata_base(self) -> str:
|
|
236
235
|
"""Detection needs BOTH signals — neither alone is sufficient."""
|
|
237
|
-
if not
|
|
236
|
+
if not looks_like_a_machine():
|
|
238
237
|
raise AuthenticationError(_NO_CREDENTIALS)
|
|
239
238
|
gateway = detect_gateway()
|
|
240
239
|
if not gateway or not probe(gateway, METADATA_PORT):
|
|
@@ -277,7 +276,7 @@ class AsyncCredentials(_CredentialsBase):
|
|
|
277
276
|
try:
|
|
278
277
|
async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECS) as client:
|
|
279
278
|
resp = await client.get(
|
|
280
|
-
base + METADATA_TOKEN_PATH
|
|
279
|
+
base + METADATA_TOKEN_PATH
|
|
281
280
|
)
|
|
282
281
|
except httpx.HTTPError as e:
|
|
283
282
|
raise AuthenticationError(f"in-VM token request failed: {e}") from e
|
|
@@ -289,7 +288,7 @@ class AsyncCredentials(_CredentialsBase):
|
|
|
289
288
|
self.discovered_base_url = api_url
|
|
290
289
|
|
|
291
290
|
async def _metadata_base(self) -> str:
|
|
292
|
-
if not
|
|
291
|
+
if not looks_like_a_machine():
|
|
293
292
|
raise AuthenticationError(_NO_CREDENTIALS)
|
|
294
293
|
gateway = detect_gateway()
|
|
295
294
|
if not gateway or not await probe_async(gateway, METADATA_PORT):
|
|
@@ -141,42 +141,6 @@ def test_both_signals_mint_a_token_and_report_the_cluster(monkeypatch):
|
|
|
141
141
|
assert c.discovered_base_url == "http://boxd-stg.sh:9443"
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def test_identity_is_forwarded_when_set(monkeypatch):
|
|
145
|
-
monkeypatch.setenv("BOXD_VM_ID", "vm-123")
|
|
146
|
-
monkeypatch.setenv("BOXD_IDENTITY", "signed-identity")
|
|
147
|
-
monkeypatch.setattr("boxd._credentials.detect_gateway", lambda: "10.42.0.1")
|
|
148
|
-
monkeypatch.setattr("boxd._credentials.probe", lambda *a, **k: True)
|
|
149
|
-
|
|
150
|
-
seen = {}
|
|
151
|
-
|
|
152
|
-
class FakeResponse:
|
|
153
|
-
status_code = 200
|
|
154
|
-
text = "{}"
|
|
155
|
-
|
|
156
|
-
def json(self):
|
|
157
|
-
return {"token": "vm-jwt", "expires_at": time.time() + 900}
|
|
158
|
-
|
|
159
|
-
class FakeClient:
|
|
160
|
-
def __init__(self, **kwargs):
|
|
161
|
-
pass
|
|
162
|
-
|
|
163
|
-
def __enter__(self):
|
|
164
|
-
return self
|
|
165
|
-
|
|
166
|
-
def __exit__(self, *a):
|
|
167
|
-
return False
|
|
168
|
-
|
|
169
|
-
def get(self, url, headers=None):
|
|
170
|
-
seen.update(headers or {})
|
|
171
|
-
return FakeResponse()
|
|
172
|
-
|
|
173
|
-
monkeypatch.setattr("boxd._credentials.httpx.Client", FakeClient)
|
|
174
|
-
creds().token()
|
|
175
|
-
assert seen["X-Boxd-Identity"] == "signed-identity"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# --- gateway parsing ---
|
|
179
|
-
|
|
180
144
|
|
|
181
145
|
def test_detect_gateway_parses_the_default_route(monkeypatch, tmp_path):
|
|
182
146
|
route = tmp_path / "route"
|
|
@@ -303,3 +267,19 @@ async def test_async_no_credentials_raises(monkeypatch):
|
|
|
303
267
|
c = AsyncCredentials(api_key=None, token=None, exchange_url=EXCHANGE)
|
|
304
268
|
with pytest.raises(AuthenticationError, match="no credentials"):
|
|
305
269
|
await c.token()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_in_vm_detection_does_not_hinge_on_one_env_var(monkeypatch):
|
|
273
|
+
# A real dev machine's shell had BOXD_IDENTITY and BOXD_PROXY_ENDPOINT but
|
|
274
|
+
# no BOXD_VM_ID — gating on BOXD_VM_ID alone disabled in-VM auth there.
|
|
275
|
+
from boxd import _credentials as c
|
|
276
|
+
|
|
277
|
+
for v in c._VM_HINTS:
|
|
278
|
+
monkeypatch.delenv(v, raising=False)
|
|
279
|
+
assert c.looks_like_a_machine() is False
|
|
280
|
+
|
|
281
|
+
for present in c._VM_HINTS:
|
|
282
|
+
for v in c._VM_HINTS:
|
|
283
|
+
monkeypatch.delenv(v, raising=False)
|
|
284
|
+
monkeypatch.setenv(present, "x")
|
|
285
|
+
assert c.looks_like_a_machine() is True, present
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|