Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-28 09:37:31

0001 """TeamComms proxy and current devcloud authentication attestations.
0002 
0003 The monitor obtains identity through authenticated introspection. Incoming
0004 identity assertions and user credentials are never forwarded to the monitor.
0005 See docs/teamcomms.md for the cross-host contract.
0006 """
0007 
0008 from datetime import timedelta
0009 import hashlib
0010 from importlib import import_module
0011 import json
0012 import logging
0013 import secrets
0014 from types import SimpleNamespace
0015 from urllib.parse import urlsplit
0016 
0017 import httpx
0018 from django.conf import settings
0019 from django.contrib.auth import get_user
0020 from django.contrib.auth.views import redirect_to_login
0021 from django.core.exceptions import RequestDataTooBig
0022 from django.http import HttpResponse, JsonResponse, StreamingHttpResponse
0023 from django.middleware.csrf import CsrfViewMiddleware, get_token
0024 from django.utils import timezone
0025 from django.views.decorators.csrf import csrf_exempt
0026 
0027 from .models import ApiToken, TeamCommsAuthReference
0028 from .token_auth import TOKEN_PREFIX, _hash
0029 
0030 logger = logging.getLogger(__name__)
0031 PUBLIC_PREFIX = '/prod/teamcomms'
0032 REFERENCE_SECONDS = 60
0033 BODY_LIMIT = 65536
0034 TIMEOUT = httpx.Timeout(connect=10, read=35, write=10, pool=10)
0035 
0036 
0037 def _json(data, status=200):
0038     response = JsonResponse(data, status=status)
0039     response['Cache-Control'] = 'no-store'
0040     return response
0041 
0042 
0043 def _current_user(reference):
0044     if reference.token_id is not None:
0045         token = reference.token
0046         if token.revoked is not None or token.user_id != reference.user_id:
0047             return None
0048         user = token.user
0049     else:
0050         store = import_module(settings.SESSION_ENGINE).SessionStore(reference.session_key)
0051         user = get_user(SimpleNamespace(session=store))
0052     if not user.is_authenticated or not user.is_active or user.pk != reference.user_id:
0053         return None
0054     return user
0055 
0056 
0057 @csrf_exempt
0058 def introspect(request):
0059     """Return current identity to the monitor's dedicated service credential."""
0060     expected = settings.SWF_TEAMCOMMS_SERVICE_TOKEN
0061     if not expected:
0062         return _json({'error': 'TeamComms authentication unavailable'}, 503)
0063     auth = request.META.get('HTTP_AUTHORIZATION', '')
0064     if not secrets.compare_digest(auth, 'Bearer ' + expected):
0065         return _json({'error': 'Service authentication required'}, 401)
0066     if request.method != 'POST':
0067         return _json({'error': 'POST required'}, 405)
0068     try:
0069         if int(request.META.get('CONTENT_LENGTH') or 0) > 1024:
0070             return _json({'error': 'Request too large'}, 413)
0071         body = request.body
0072         if len(body) > 1024:
0073             return _json({'error': 'Request too large'}, 413)
0074         data = json.loads(body)
0075         raw = data.get('reference') if isinstance(data, dict) else None
0076         if not isinstance(raw, str) or len(raw) != 64:
0077             return _json({'error': 'Invalid authentication reference'}, 401)
0078         reference = TeamCommsAuthReference.objects.select_related('token__user').filter(
0079             key_hash=_hash(raw), expires_at__gt=timezone.now(),
0080         ).first()
0081         if reference is None:
0082             return _json({'error': 'Expired or invalid authentication reference'}, 401)
0083         user = _current_user(reference)
0084         if user is None:
0085             return _json({'error': 'Authentication revoked or expired'}, 401)
0086         human = {'subject': str(user.pk), 'username': user.username,
0087                  'name': user.get_full_name() or user.username}
0088         identity = dict(human, kind='human')
0089         if reference.token_id is not None and reference.token.teamcomms_ai:
0090             identity.update(subject='ai:' + str(user.pk), kind='ai',
0091                             name=human['name'] + ' AI', operator=human)
0092         elif reference.token_id is not None and reference.token.teamcomms_service_kind:
0093             kind = reference.token.teamcomms_service_kind
0094             if kind not in {'program', 'connector'}:
0095                 return _json({'error': 'Invalid service identity'}, 401)
0096             identity.update(subject=kind + ':' + str(user.pk), kind=kind,
0097                             name=human['name'] + ' ' + kind, account_subject=str(user.pk))
0098         return _json(dict(identity,
0099             auth_method='token' if reference.token_id is not None else 'session',
0100             csrf_verified=reference.csrf_verified,
0101             method=reference.method, path=reference.path,
0102             query_string=reference.query_string, body_sha256=reference.body_sha256,
0103             expires_at=reference.expires_at.isoformat()))
0104     except (ValueError, RequestDataTooBig):
0105         return _json({'error': 'Invalid introspection request'}, 400)
0106     except Exception as error:
0107         logger.error('TeamComms introspection unavailable (%s)', type(error).__name__)
0108         return _json({'error': 'Authentication authority unavailable'}, 503)
0109 
0110 
0111 def _authenticated_request(request):
0112     """Resolve a token explicitly, or require a current browser session and CSRF."""
0113     auth = request.META.get('HTTP_AUTHORIZATION', '')
0114     if auth:
0115         if not auth.startswith('Bearer ' + TOKEN_PREFIX):
0116             return None, _json({'error': 'A devcloud bearer token is required'}, 401)
0117         token = ApiToken.objects.select_related('user').filter(
0118             key_hash=_hash(auth[7:].strip()), revoked__isnull=True,
0119             user__is_active=True,
0120         ).first()
0121         if token is None:
0122             return None, _json({'error': 'Invalid or revoked token'}, 401)
0123         request.user = token.user
0124         return token, None
0125     if not request.user.is_authenticated or not request.user.is_active:
0126         return None, _json({'error': 'Sign in or supply a devcloud bearer token'}, 401)
0127     # The outer view is exempt so bearer calls need no cookie CSRF. Validate
0128     # cookie calls with the ordinary Django middleware and a non-exempt view.
0129     csrf = CsrfViewMiddleware(lambda req: HttpResponse())
0130     rejection = csrf.process_view(request, lambda req: HttpResponse(), (), {})
0131     if rejection is not None:
0132         return None, _json({'error': 'CSRF validation failed'}, 403)
0133     return None, None
0134 
0135 
0136 def _response_headers(upstream, response):
0137     for key in ('Content-Type', 'MCP-Session-Id', 'MCP-Protocol-Version', 'Allow',
0138                 'Retry-After', 'WWW-Authenticate', 'Content-Security-Policy',
0139                 'X-Content-Type-Options', 'Referrer-Policy'):
0140         if key in upstream.headers:
0141             response[key] = upstream.headers[key]
0142     response['Cache-Control'] = 'no-store'
0143     if 'location' in upstream.headers:
0144         target = urlsplit(upstream.headers['location'])
0145         prefix = '/swf-monitor/teamcomms'
0146         if target.path.startswith(prefix + '/') or target.path == prefix:
0147             response['Location'] = PUBLIC_PREFIX + target.path[len(prefix):]
0148             if target.query:
0149                 response['Location'] += '?' + target.query
0150         elif target.path.startswith(PUBLIC_PREFIX + '/') or target.path == PUBLIC_PREFIX:
0151             response['Location'] = target.path + ('?' + target.query if target.query else '')
0152     return response
0153 
0154 
0155 def browser_csrf(request):
0156     """Supply this origin's masked CSRF token using its existing browser session."""
0157     if request.method != 'GET':
0158         return _json({'error': 'GET required'}, 405)
0159     if (request.META.get('HTTP_AUTHORIZATION') or not request.user.is_authenticated
0160             or not request.user.is_active):
0161         return _json({'error': 'An active devcloud browser session is required'}, 401)
0162     # CsrfViewMiddleware emits the host cookie, including when none was present.
0163     return _json({'header_name': 'X-CSRFToken', 'token': get_token(request)})
0164 
0165 
0166 @csrf_exempt
0167 def proxy(request, subpath=''):
0168     """Relay authenticated TC HTTP/MCP and bounded SSE through the existing tunnel."""
0169     if not settings.SWF_TEAMCOMMS_SERVICE_TOKEN:
0170         return _json({'error': 'TeamComms authentication unavailable'}, 503)
0171     client = None
0172     upstream = None
0173     try:
0174         token, rejection = _authenticated_request(request)
0175         if rejection is not None:
0176             if (rejection.status_code == 401 and request.method == 'GET'
0177                     and not request.META.get('HTTP_AUTHORIZATION')
0178                     and 'text/html' in request.headers.get('Accept', '')
0179                     and (subpath in {'', 'entries', 'sessions', 'dialog'}
0180                          or subpath.startswith('entries/'))):
0181                 return redirect_to_login(request.get_full_path())
0182             return rejection
0183         if int(request.META.get('CONTENT_LENGTH') or 0) > BODY_LIMIT:
0184             return _json({'error': 'Request too large'}, 413)
0185         body = request.body
0186         if len(body) > BODY_LIMIT:
0187             return _json({'error': 'Request too large'}, 413)
0188         # Reject path normalization before building an upstream URL: a TC
0189         # request must not escape its dedicated authentication boundary.
0190         if any(part in {'.', '..'} for part in subpath.split('/')) or any(c in subpath for c in '%\\?#'):
0191             return _json({'error': 'Invalid TeamComms path'}, 400)
0192         raw = secrets.token_hex(32)
0193         now = timezone.now()
0194         TeamCommsAuthReference.objects.create(
0195             key_hash=_hash(raw), user=request.user, token=token,
0196             session_key='' if token else request.session.session_key,
0197             method=request.method, path='/' + subpath,
0198             query_string=request.META.get('QUERY_STRING', ''),
0199             body_sha256=hashlib.sha256(body).hexdigest(),
0200             csrf_verified=token is None,
0201             expires_at=now + timedelta(seconds=REFERENCE_SECONDS),
0202         )
0203         # These are temporary authentication records, not retained team data.
0204         TeamCommsAuthReference.objects.filter(expires_at__lte=now).delete()
0205         headers = {
0206             'Host': 'epic-devcloud.org',
0207             'X-TeamComms-Auth-Ref': raw,
0208             'X-Forwarded-Host': 'epic-devcloud.org',
0209             'X-Forwarded-Proto': 'https',
0210             'Accept-Encoding': 'identity',
0211         }
0212         for key in ('Accept', 'Content-Type', 'Origin', 'Last-Event-ID',
0213                     'MCP-Protocol-Version', 'MCP-Session-Id'):
0214             if key in request.headers:
0215                 headers[key] = request.headers[key]
0216         url = settings.SWF_MONITOR_URL.rstrip('/') + '/teamcomms/' + subpath
0217         query = request.META.get('QUERY_STRING', '')
0218         if query:
0219             url += '?' + query
0220         client = httpx.Client(timeout=TIMEOUT, verify=False, follow_redirects=False)
0221         upstream = client.send(client.build_request(request.method, url, headers=headers, content=body), stream=True)
0222         if upstream.headers.get('content-type', '').split(';', 1)[0] == 'text/event-stream':
0223             source, connection = upstream, client
0224 
0225             def chunks():
0226                 try:
0227                     yield from source.iter_raw()
0228                 except httpx.HTTPError as error:
0229                     logger.error('TeamComms stream failed (%s)', type(error).__name__)
0230                     yield b'event: error\ndata: {"error":"TeamComms tunnel unavailable","status":502}\n\n'
0231                 finally:
0232                     source.close()
0233                     connection.close()
0234 
0235             response = StreamingHttpResponse(chunks(), status=upstream.status_code,
0236                                              content_type='text/event-stream')
0237             # Close even when the downstream disconnects before iteration.
0238             response._resource_closers.extend([source.close, connection.close])
0239             response['X-Accel-Buffering'] = 'no'
0240             _response_headers(upstream, response)
0241             upstream = client = None
0242             return response
0243         content = upstream.read()
0244         return _response_headers(upstream, HttpResponse(content, status=upstream.status_code))
0245     except (ValueError, RequestDataTooBig):
0246         return _json({'error': 'Invalid TeamComms request'}, 400)
0247     except httpx.HTTPError as error:
0248         logger.error('TeamComms proxy failed (%s)', type(error).__name__)
0249         return _json({'error': 'TeamComms tunnel unavailable'}, 502)
0250     except Exception as error:
0251         logger.error('TeamComms proxy unavailable (%s)', type(error).__name__)
0252         return _json({'error': 'TeamComms authentication unavailable'}, 503)
0253     finally:
0254         if upstream is not None:
0255             upstream.close()
0256         if client is not None:
0257             client.close()