From e4a5a6b5615cbc5c80fe277a11ea9a7efc762571 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 14 Feb 2024 14:07:43 +0100 Subject: [PATCH] Fix: Implemented CORS support on supervisor endpoints. --- packaging/Makefile | 2 +- pyproject.toml | 3 +- src/aleph/vm/orchestrator/supervisor.py | 7 +- src/aleph/vm/orchestrator/views/__init__.py | 107 +++++++++++++++++++- src/aleph/vm/orchestrator/views/operator.py | 46 +++++++++ 5 files changed, 156 insertions(+), 9 deletions(-) diff --git a/packaging/Makefile b/packaging/Makefile index 0226808c9..a1d44026e 100644 --- a/packaging/Makefile +++ b/packaging/Makefile @@ -15,7 +15,7 @@ debian-package-code: cp ../examples/instance_message_from_aleph.json ./aleph-vm/opt/aleph-vm/examples/instance_message_from_aleph.json cp -r ../examples/data ./aleph-vm/opt/aleph-vm/examples/data mkdir -p ./aleph-vm/opt/aleph-vm/examples/volumes - pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.2' 'jwskate==0.8.0' 'eth-account==0.9.0' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'superfluid==0.2.1' 'sqlalchemy[asyncio]' 'aiosqlite==0.19.0' 'alembic==1.13.1' + pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.2' 'jwskate==0.8.0' 'eth-account==0.9.0' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'superfluid==0.2.1' 'sqlalchemy[asyncio]' 'aiosqlite==0.19.0' 'alembic==1.13.1' 'aiohttp_cors==0.7.0' python3 -m compileall ./aleph-vm/opt/aleph-vm/ debian-package-resources: firecracker-bins vmlinux download-ipfs-kubo diff --git a/pyproject.toml b/pyproject.toml index bd9f1474b..41d2198bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,8 @@ dependencies = [ "superfluid~=0.2.1", "sqlalchemy[asyncio]", "aiosqlite==0.19.0", - "alembic==1.13.1" + "alembic==1.13.1", + "aiohttp_cors~=0.7.0", ] [project.urls] diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index e541539a5..674c44ee4 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -13,6 +13,7 @@ from secrets import token_urlsafe from typing import Callable +import aiohttp_cors from aiohttp import web from aleph.vm.conf import settings @@ -68,9 +69,6 @@ async def server_version_middleware( return resp -app = web.Application(middlewares=[server_version_middleware]) - - async def allow_cors_on_endpoint(request: web.Request): """Allow CORS on endpoints that VM owners use to control their machine.""" return web.Response( @@ -84,6 +82,9 @@ async def allow_cors_on_endpoint(request: web.Request): ) +app = web.Application(middlewares=[server_version_middleware]) +cors = aiohttp_cors.setup(app) + app.add_routes( [ # /about APIs return information about the VM Orchestrator diff --git a/src/aleph/vm/orchestrator/views/__init__.py b/src/aleph/vm/orchestrator/views/__init__.py index 7d516d928..c8ebf4e5e 100644 --- a/src/aleph/vm/orchestrator/views/__init__.py +++ b/src/aleph/vm/orchestrator/views/__init__.py @@ -5,6 +5,7 @@ from hashlib import sha256 from json import JSONDecodeError from pathlib import Path +from secrets import compare_digest from string import Template from typing import Optional @@ -12,6 +13,7 @@ import aiohttp from aiohttp import web from aiohttp.web_exceptions import HTTPNotFound +from aiohttp_cors import ResourceOptions, custom_cors from aleph_message.exceptions import UnknownHashError from aleph_message.models import ItemHash, MessageType from pydantic import ValidationError @@ -110,9 +112,18 @@ def authenticate_request(request: web.Request) -> None: raise web.HTTPUnauthorized(reason="Invalid token", text="401 Invalid token") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def about_login(request: web.Request) -> web.Response: token = request.query.get("token") - if token == request.app["secret_token"]: + if compare_digest(token, request.app["secret_token"]): response = web.HTTPFound("/about/config") response.cookies["token"] = token return response @@ -120,6 +131,15 @@ async def about_login(request: web.Request) -> web.Response: return web.json_response({"success": False}, status=401) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def about_executions(request: web.Request) -> web.Response: authenticate_request(request) pool: VmPool = request.app["vm_pool"] @@ -129,6 +149,15 @@ async def about_executions(request: web.Request) -> web.Response: ) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def list_executions(request: web.Request) -> web.Response: pool: VmPool = request.app["vm_pool"] return web.json_response( @@ -143,7 +172,6 @@ async def list_executions(request: web.Request) -> web.Response: if execution.is_running }, dumps=dumps_for_json, - headers={"Access-Control-Allow-Origin": "*"}, ) @@ -155,9 +183,18 @@ async def about_config(request: web.Request) -> web.Response: ) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def about_execution_records(_: web.Request): records = await get_execution_records() - return web.json_response(records, dumps=dumps_for_json, headers={"Access-Control-Allow-Origin": "*"}) + return web.json_response(records, dumps=dumps_for_json) async def index(request: web.Request): @@ -174,6 +211,15 @@ async def index(request: web.Request): return web.Response(content_type="text/html", body=body) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_check_fastapi(request: web.Request, vm_id: Optional[ItemHash] = None): """Check that the FastAPI diagnostic VM runs correctly""" @@ -215,11 +261,29 @@ async def status_check_fastapi(request: web.Request, vm_id: Optional[ItemHash] = ) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_check_fastapi_legacy(request: web.Request): """Check that the legacy FastAPI VM runs correctly""" return await status_check_fastapi(request, vm_id=ItemHash(settings.LEGACY_CHECK_FASTAPI_VM_ID)) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_check_host(request: web.Request): """Check that the platform is supported and configured correctly""" @@ -239,6 +303,15 @@ async def status_check_host(request: web.Request): return web.json_response(result, status=result_status, headers={"Access-Control-Allow-Origin": "*"}) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_check_ipv6(request: web.Request): """Check that the platform has IPv6 egress connectivity""" timeout = aiohttp.ClientTimeout(total=2) @@ -252,6 +325,15 @@ async def status_check_ipv6(request: web.Request): return web.json_response(result, headers={"Access-Control-Allow-Origin": "*"}) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_check_version(request: web.Request): """Check if the software is running a version equal or newer than the given one""" reference_str: Optional[str] = request.query.get("reference") @@ -277,6 +359,15 @@ async def status_check_version(request: web.Request): return web.HTTPForbidden(text=f"Outdated: version {current} < {reference}") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def status_public_config(request: web.Request): """Expose the public fields from the configuration""" return web.json_response( @@ -414,6 +505,15 @@ async def update_allocations(request: web.Request): ) +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def notify_allocation(request: web.Request): """Notify instance allocation, only used for Pay as you Go feature""" try: @@ -501,5 +601,4 @@ async def notify_allocation(request: web.Request): "errors": {vm_hash: repr(error) for vm_hash, error in scheduling_errors.items()}, }, status=status_code, - headers={"Access-Control-Allow-Origin": "*"}, ) diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index 052368a9c..77679ba81 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -5,6 +5,7 @@ import aiohttp.web_exceptions from aiohttp import web from aiohttp.web_urldispatcher import UrlMappingMatchInfo +from aiohttp_cors import ResourceOptions, custom_cors from aleph_message.exceptions import UnknownHashError from aleph_message.models import ItemHash from aleph_message.models.execution import BaseExecutableContent @@ -50,6 +51,15 @@ def is_sender_authorized(authenticated_sender: str, message: BaseExecutableConte return False +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) async def stream_logs(request: web.Request) -> web.StreamResponse: """Stream the logs of a VM. @@ -105,6 +115,15 @@ async def authenticate_for_vm_or_403(execution, request, vm_hash, ws): raise web.HTTPForbidden(body="Unauthorized sender") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) @require_jwk_authentication async def operate_expire(request: web.Request, authenticated_sender: str) -> web.Response: """Stop the virtual machine, smoothly if possible. @@ -131,6 +150,15 @@ async def operate_expire(request: web.Request, authenticated_sender: str) -> web return web.Response(status=200, body=f"Expiring VM with ref {vm_hash} in {timeout} seconds") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) @require_jwk_authentication async def operate_stop(request: web.Request, authenticated_sender: str) -> web.Response: """Stop the virtual machine, smoothly if possible.""" @@ -155,6 +183,15 @@ async def operate_stop(request: web.Request, authenticated_sender: str) -> web.R return web.Response(status=200, body="Already stopped, nothing to do") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) @require_jwk_authentication async def operate_reboot(request: web.Request, authenticated_sender: str) -> web.Response: """ @@ -181,6 +218,15 @@ async def operate_reboot(request: web.Request, authenticated_sender: str) -> web return web.Response(status=200, body="Starting VM (was not running) with ref {vm_hash}") +@custom_cors( + { + "*": ResourceOptions( + allow_credentials=True, + allow_headers="*", + expose_headers="*", + ) + } +) @require_jwk_authentication async def operate_erase(request: web.Request, authenticated_sender: str) -> web.Response: """Delete all data stored by a virtual machine.