An installation can be enrolled with a portal by redeeming a claim code, after which it holds one authenticated websocket open and answers proxied API calls over it. Requests are dispatched into this process's own ASGI app, so the HTTP trigger routes flows install at runtime are visible to it, and the live flow stream is bridged straight off the event bus. decode_token grows the third branch its docstring anticipated: tokens signed by the enrolled portal resolve to the local account that performed the enrolment, verified against a JWKS pinned at that moment. With no enrolment the branch raises immediately, so an offline installation is unchanged and untouched. Disconnecting deletes one file, which is the entire local revocation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtBzdDyLsmDaF1W7DLYtYM
183 lines
6.0 KiB
Python
183 lines
6.0 KiB
Python
"""Remote access: off by default, and scoped to the account that enabled it.
|
|
|
|
The property worth pinning down is the one everything else rests on — a
|
|
portal's token is worth nothing here until somebody at this installation
|
|
enrolled it, and even then it grants exactly the rights of the account that
|
|
did.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
import jwt
|
|
import pytest
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import Session, select
|
|
|
|
from app.api.deps import decode_token, user_from_token
|
|
from app.cloud import config as cloud_config
|
|
from app.core.config import settings
|
|
from app.models import User
|
|
|
|
INSTALLATION_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
|
|
ISSUER = "https://hub.example.test"
|
|
|
|
|
|
@pytest.fixture
|
|
def portal_key() -> rsa.RSAPrivateKey:
|
|
return rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
|
|
|
|
def _jwks(key: rsa.RSAPrivateKey) -> dict[str, Any]:
|
|
jwk = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(key.public_key()))
|
|
jwk.update({"use": "sig", "alg": "RS256", "kid": "test-portal"})
|
|
return {"keys": [jwk]}
|
|
|
|
|
|
def _portal_token(
|
|
key: rsa.RSAPrivateKey,
|
|
*,
|
|
subject: str = "portal-user-1",
|
|
audience: str = INSTALLATION_ID,
|
|
issuer: str = ISSUER,
|
|
scope: str = "proxy",
|
|
) -> str:
|
|
now = datetime.now(UTC)
|
|
pem = key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
return jwt.encode(
|
|
{
|
|
"sub": subject,
|
|
"iss": issuer,
|
|
"aud": audience,
|
|
"iat": now,
|
|
"exp": now + timedelta(hours=1),
|
|
"scope": scope,
|
|
},
|
|
pem,
|
|
algorithm="RS256",
|
|
headers={"kid": "test-portal"},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def enrolled(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
portal_key: rsa.RSAPrivateKey,
|
|
db: Session,
|
|
) -> Any:
|
|
"""Enrol this installation with a fake portal, then undo it."""
|
|
local_user = db.exec(
|
|
select(User).where(User.email == settings.FIRST_SUPERUSER)
|
|
).one()
|
|
original = settings.CLOUD_CONFIG_FILE
|
|
settings.CLOUD_CONFIG_FILE = (
|
|
tmp_path_factory.mktemp(f"cloud-{uuid.uuid4().hex[:6]}") / "cloud.json"
|
|
)
|
|
cloud_config.save(
|
|
cloud_config.CloudConfig(
|
|
portal_url=ISSUER,
|
|
ws_url=f"{ISSUER}/api/v1/tunnel/attach",
|
|
installation_id=INSTALLATION_ID,
|
|
token="installation-token",
|
|
issuer=ISSUER,
|
|
jwks=_jwks(portal_key),
|
|
local_user_id=str(local_user.id),
|
|
enrolled_at=datetime.now(UTC).isoformat(),
|
|
portal_account=settings.FIRST_SUPERUSER,
|
|
)
|
|
)
|
|
yield local_user
|
|
cloud_config.delete()
|
|
settings.CLOUD_CONFIG_FILE = original
|
|
|
|
|
|
def test_portal_token_is_refused_when_not_enrolled(
|
|
portal_key: rsa.RSAPrivateKey, tmp_path_factory: pytest.TempPathFactory
|
|
) -> None:
|
|
"""An installation nobody connected trusts no portal at all."""
|
|
original = settings.CLOUD_CONFIG_FILE
|
|
settings.CLOUD_CONFIG_FILE = tmp_path_factory.mktemp("empty") / "cloud.json"
|
|
try:
|
|
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
|
decode_token(_portal_token(portal_key))
|
|
finally:
|
|
settings.CLOUD_CONFIG_FILE = original
|
|
|
|
|
|
def test_portal_token_resolves_to_the_enrolling_user(
|
|
enrolled: User, portal_key: rsa.RSAPrivateKey, db: Session
|
|
) -> None:
|
|
claims = decode_token(_portal_token(portal_key))
|
|
assert claims["sub"] == str(enrolled.id)
|
|
# Who they are on the portal is carried for the record, not for rights.
|
|
assert claims["portal_sub"] == "portal-user-1"
|
|
assert user_from_token(db, _portal_token(portal_key)) == enrolled
|
|
|
|
|
|
def test_token_for_another_installation_is_refused(
|
|
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
|
portal_key: rsa.RSAPrivateKey,
|
|
) -> None:
|
|
"""The audience is this installation's id, so someone else's is worthless."""
|
|
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
|
decode_token(_portal_token(portal_key, audience=str(uuid.uuid4())))
|
|
|
|
|
|
def test_token_from_an_unpinned_key_is_refused(
|
|
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
|
) -> None:
|
|
"""A different portal, or a hijacked one, cannot sign for this installation."""
|
|
impostor = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
|
decode_token(_portal_token(impostor))
|
|
|
|
|
|
def test_non_proxy_scope_is_refused(
|
|
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
|
portal_key: rsa.RSAPrivateKey,
|
|
) -> None:
|
|
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
|
decode_token(_portal_token(portal_key, scope="session"))
|
|
|
|
|
|
def test_disconnecting_ends_remote_access(
|
|
enrolled: User, portal_key: rsa.RSAPrivateKey
|
|
) -> None:
|
|
"""Deleting the config is the whole of the local revocation."""
|
|
assert decode_token(_portal_token(portal_key))["sub"] == str(enrolled.id)
|
|
cloud_config.delete()
|
|
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
|
decode_token(_portal_token(portal_key))
|
|
|
|
|
|
def test_status_reports_not_enrolled(
|
|
client: TestClient, superuser_token_headers: dict[str, str]
|
|
) -> None:
|
|
response = client.get(
|
|
f"{settings.API_V1_STR}/cloud/status", headers=superuser_token_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["enrolled"] is False
|
|
|
|
|
|
def test_enrolling_needs_a_superuser(
|
|
client: TestClient, normal_user_token_headers: dict[str, str]
|
|
) -> None:
|
|
"""Remote access is an installation-wide grant, not a personal setting."""
|
|
response = client.post(
|
|
f"{settings.API_V1_STR}/cloud/enroll",
|
|
headers=normal_user_token_headers,
|
|
json={"portal_url": ISSUER, "claim_code": "ABCD-EFGH"},
|
|
)
|
|
assert response.status_code == 403
|