Pair a wall panel through the portal
A screen somewhere this installation is not reachable from asks the portal for a code instead, and the portal mints its credential — because a token signed here is one such a device could never present. Where it was minted changes nothing about what it may do. The panel gate moved off the branch that decodes a local panel token and onto whatever claims name a panel, so the portal's and this installation's are bounded by the same check against the same panel's dashboards. A token of that scope naming no panel is refused rather than left holding the account it borrows. The connector marks what arrives on its socket, since that is the only thing that makes it true, and the approval screen now names what is holding a code — approving adopts whatever answers, so it is worth a look first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017F9RnYCJgASuBTcAjxmnsp
This commit is contained in:
@@ -121,7 +121,19 @@ def decode_portal_token(token: str) -> dict[str, Any]:
|
||||
audience=config.installation_id,
|
||||
issuer=config.issuer,
|
||||
)
|
||||
if claims.get("scope") != "proxy":
|
||||
scope = claims.get("scope")
|
||||
if scope == "panel":
|
||||
# A screen that paired through the portal. The portal named the panel
|
||||
# and nothing else; what that panel may read is decided here, by the
|
||||
# same check a panel paired on this network passes. It acts as the
|
||||
# enrolling account for want of any other, but the scope check is what
|
||||
# actually bounds it — so a token of this scope that names no panel is
|
||||
# refused rather than left holding the account it borrows.
|
||||
panel = str(claims.get("sub") or "")
|
||||
if not panel:
|
||||
raise InvalidTokenError("a panel token must name its panel")
|
||||
return {"sub": config.local_user_id, "panel": panel}
|
||||
if scope != "proxy":
|
||||
raise InvalidTokenError("not a proxy token")
|
||||
# Every portal session acts as the enrolling local user. Who they are on
|
||||
# the portal is kept for the audit trail, not for authorization.
|
||||
|
||||
@@ -63,6 +63,9 @@ class CloudConnector:
|
||||
"enrolled": config is not None,
|
||||
"connected": self._connected,
|
||||
"portal_url": config.portal_url if config else None,
|
||||
# Where a browser reaches the portal, which is not always where
|
||||
# this process does: enrolment may have named a container.
|
||||
"issuer": config.issuer if config else None,
|
||||
"portal_account": config.portal_account if config else None,
|
||||
"installation_id": config.installation_id if config else None,
|
||||
"last_error": self._last_error,
|
||||
@@ -215,11 +218,18 @@ class CloudConnector:
|
||||
content = base64.b64decode(body) if body else None
|
||||
query = str(frame.get("query") or "")
|
||||
|
||||
headers = dict(frame.get("headers") or {})
|
||||
# Set here rather than trusted from the frame: a browser can send
|
||||
# any header it likes through the proxy, and this one decides where
|
||||
# a pairing device's credential is minted. Arriving on this socket
|
||||
# is the only thing that makes it true.
|
||||
headers["x-fluksio-via"] = "portal"
|
||||
|
||||
async with self._client() as client:
|
||||
request = client.build_request(
|
||||
str(frame.get("method") or "GET"),
|
||||
f"{path}?{query}" if query else path,
|
||||
headers=dict(frame.get("headers") or {}),
|
||||
headers=headers,
|
||||
content=content,
|
||||
)
|
||||
response = await client.send(request, stream=True)
|
||||
|
||||
Reference in New Issue
Block a user