Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:37

0001 """Devcloud authentication and monitor authorization for embedded TeamComms.
0002 
0003 The public proxy supplies an opaque reference to a single authenticated request.
0004 Devcloud revalidates its underlying session or token on every introspection.
0005 The monitor retains the authoritative component permissions and database.
0006 """
0007 
0008 import hashlib
0009 import hmac
0010 import logging
0011 from datetime import datetime, timezone
0012 from pathlib import Path
0013 from urllib.parse import urlsplit
0014 
0015 import httpx
0016 from django.conf import settings
0017 from django.contrib.auth import get_user_model
0018 from starlette.responses import JSONResponse
0019 from teamcomms.service.access import AccessError
0020 from teamcomms.service.dispatch import database_call
0021 from teamcomms.service.embedded import HostAuthentication, HostIdentity
0022 
0023 from .authority import get_authority, is_ops
0024 
0025 logger = logging.getLogger(__name__)
0026 BACKEND_PREFIX = "/swf-monitor/teamcomms"
0027 PUBLIC_PREFIX = "/prod/teamcomms"
0028 TEAM_ID = "8873a572-a319-4a90-bfc9-76a9b76e30fb"
0029 READ_SCOPES = frozenset({"directory:read", "entries:read", "comms:read", "dialog:read", "inflight:read", "capcom:read"})
0030 WRITE_SCOPES = frozenset({"entries:write", "sessions:write", "comms:write", "dialog:write", "inflight:write", "capcom:write"})
0031 
0032 
0033 def one_header(scope, name):
0034     values = [value.decode("latin1") for key, value in scope.get("headers", [])
0035               if key.lower() == name]
0036     if len(values) != 1 or not values[0]:
0037         raise AccessError("TeamComms proxy authentication required", 401)
0038     return values[0]
0039 
0040 
0041 def component_permissions(username):
0042     account = get_user_model().objects.filter(username=username).only("is_active").first()
0043     if account is not None and not account.is_active:
0044         raise AccessError("Monitor account is inactive", 401)
0045     authority = get_authority(username)
0046     return READ_SCOPES | WRITE_SCOPES, "admin" if is_ops(authority) else "member"
0047 
0048 
0049 class RemoteAuthentication(HostAuthentication):
0050     def __init__(self):
0051         super().__init__(provider="swf-remote", team_id=TEAM_ID,
0052                          resolve=self.resolve_request, revalidate=self.revalidate_request,
0053                          check_csrf=self.check_request_csrf)
0054         self.client = httpx.AsyncClient(timeout=3.0, follow_redirects=False, trust_env=False)
0055 
0056     async def authenticate(self, scope, body):
0057         # TC has already bounded the body to 64 KiB. Bind token requests as well
0058         # as cookie requests before the base class maps or admits an identity.
0059         scope["teamcomms.body_sha256"] = hashlib.sha256(body).hexdigest()
0060         return await super().authenticate(scope, body)
0061 
0062     async def introspect(self, scope):
0063         reference = one_header(scope, b"x-teamcomms-auth-ref")
0064         if len(reference) > 256:
0065             raise AccessError("Invalid TeamComms authentication reference", 401)
0066         endpoint = settings.SWF_TEAMCOMMS_INTROSPECTION_URL
0067         secret_file = settings.SWF_TEAMCOMMS_SERVICE_TOKEN_FILE
0068         try:
0069             secret = Path(secret_file).read_text().strip() if secret_file else ""
0070         except OSError:
0071             raise AccessError("TeamComms service credential unavailable", 503) from None
0072         target = urlsplit(endpoint)
0073         if (target.scheme != "https" or not target.hostname or target.username
0074                 or target.password or target.fragment or not secret):
0075             raise AccessError("TeamComms authentication is not configured", 503)
0076         try:
0077             response = await self.client.post(endpoint, json={"reference": reference},
0078                                               headers={"Authorization": f"Bearer {secret}"})
0079         except httpx.HTTPError as error:
0080             logger.warning("TeamComms introspection unavailable (%s)", type(error).__name__)
0081             raise AccessError("Authentication authority unavailable", 503) from None
0082         if response.status_code in (401, 403):
0083             raise AccessError("Authentication expired, revoked or denied", response.status_code)
0084         if response.status_code != 200:
0085             raise AccessError("Authentication authority unavailable", 503)
0086         try:
0087             data = response.json()
0088             expires = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00"))
0089             if expires.tzinfo is None:
0090                 raise ValueError("Expiry requires a timezone")
0091             if expires <= datetime.now(timezone.utc):
0092                 raise AccessError("Authentication reference expired", 401)
0093             suffix = scope["path"].removeprefix(PUBLIC_PREFIX)
0094             if (data["method"] != scope["method"] or data["path"] != suffix
0095                     or data["query_string"] != scope.get("query_string", b"").decode("latin1")
0096                     or not hmac.compare_digest(data["body_sha256"], scope["teamcomms.body_sha256"])):
0097                 raise AccessError("Authentication reference does not match request", 401)
0098             if data["auth_method"] not in ("session", "token"):
0099                 raise ValueError("Unknown authentication method")
0100             if not isinstance(data["csrf_verified"], bool):
0101                 raise ValueError("Invalid CSRF attestation")
0102         except (ValueError, TypeError, KeyError, AttributeError):
0103             raise AccessError("Invalid authentication authority response", 503) from None
0104         scope["teamcomms.attestation"] = data
0105         return data
0106 
0107     async def resolve_request(self, scope):
0108         data = await self.introspect(scope)
0109         try:
0110             subject, username, name = data["subject"], data["username"], data["name"]
0111             if not all(isinstance(value, str) and value for value in (subject, username, name)):
0112                 raise ValueError("Invalid identity")
0113             session_authenticated = data["auth_method"] == "session"
0114             scopes, role = await database_call(component_permissions, username)
0115             operator = None
0116             if data["kind"] == "ai":
0117                 owner = data["operator"]
0118                 if (session_authenticated or subject != "ai:" + owner["subject"]
0119                         or owner["username"] != username or not owner["subject"].isdigit()):
0120                     raise ValueError("Invalid AI/operator binding")
0121                 operator = HostIdentity(subject=owner["subject"], name=owner["name"],
0122                                         scopes=scopes, role=role, session_authenticated=False)
0123             elif data["kind"] in ("program", "connector"):
0124                 account_subject = data["account_subject"]
0125                 if (session_authenticated or not isinstance(account_subject, str)
0126                         or not account_subject.isascii() or not account_subject.isdecimal()
0127                         or account_subject.startswith("0")
0128                         or subject != data["kind"] + ":" + account_subject
0129                         or data.get("operator") is not None):
0130                     raise ValueError("Invalid service/account binding")
0131             elif data["kind"] != "human" or not subject.isdigit() or data.get("operator"):
0132                 raise ValueError("Invalid human binding")
0133             identity = HostIdentity(subject=subject, name=name, kind=data["kind"],
0134                                     scopes=scopes, role=role,
0135                                     session_authenticated=session_authenticated, operator=operator)
0136             identity.validate()
0137             return identity
0138         except (ValueError, TypeError, KeyError, AttributeError):
0139             raise AccessError("Invalid authentication authority identity", 503) from None
0140 
0141     async def revalidate_request(self, scope, original_identity):
0142         return await self.resolve_request(scope)
0143 
0144     async def check_request_csrf(self, scope, body, identity):
0145         return scope["teamcomms.attestation"]["csrf_verified"] is True
0146 
0147 
0148 class TrustedProxy:
0149     """Accept the designated loopback hop and restore the public URL for TC."""
0150 
0151     def __init__(self, app):
0152         self.app = app
0153 
0154     async def __call__(self, scope, receive, send):
0155         if scope["type"] != "http":
0156             return await self.app(scope, receive, send)
0157         try:
0158             if (scope.get("client") or ("", 0))[0] not in ("127.0.0.1", "::1"):
0159                 raise AccessError("TeamComms requires the authenticated devcloud proxy", 401)
0160             host = one_header(scope, b"x-forwarded-host")
0161             if host != settings.SWF_TEAMCOMMS_PUBLIC_HOST or one_header(scope, b"x-forwarded-proto") != "https":
0162                 raise AccessError("Invalid TeamComms proxy origin", 401)
0163             one_header(scope, b"x-teamcomms-auth-ref")
0164             path = scope["path"]
0165             if not (path == BACKEND_PREFIX or path.startswith(BACKEND_PREFIX + "/")):
0166                 raise AccessError("Invalid TeamComms proxy path", 404)
0167         except AccessError as error:
0168             return await JSONResponse({"error": str(error)}, status_code=error.status,
0169                                       headers={"Cache-Control": "no-store"})(scope, receive, send)
0170         scope = dict(scope)
0171         scope["path"] = PUBLIC_PREFIX + path[len(BACKEND_PREFIX):]
0172         raw = scope.get("raw_path", path.encode("utf8"))
0173         scope["raw_path"] = PUBLIC_PREFIX.encode() + raw[len(BACKEND_PREFIX):]
0174         scope["root_path"] = ""
0175         scope["scheme"] = "https"
0176         scope["server"] = (host, 443)
0177         scope["headers"] = [(key, value) for key, value in scope["headers"] if key.lower() != b"host"]
0178         scope["headers"].append((b"host", host.encode("ascii")))
0179         await self.app(scope, receive, send)