diff --git a/arcade/arcade/actor/core/auth.py b/arcade/arcade/actor/core/auth.py index e1494405..05b3af5b 100644 --- a/arcade/arcade/actor/core/auth.py +++ b/arcade/arcade/actor/core/auth.py @@ -1,12 +1,13 @@ +import logging from dataclasses import dataclass from enum import Enum import jwt -from arcade.core.config import config - SUPPORTED_TOKEN_VER = "1" # noqa: S105 Possible hardcoded password assigned (false positive) +logger = logging.getLogger(__name__) + @dataclass class TokenValidationResult: @@ -18,16 +19,23 @@ class SigningAlgorithm(str, Enum): HS256 = "HS256" -def validate_engine_token(token: str) -> TokenValidationResult: +def validate_engine_token(actor_secret: str, token: str) -> TokenValidationResult: try: payload = jwt.decode( token, - config.api.key, + actor_secret, algorithms=[SigningAlgorithm.HS256], verify=True, audience="actor", ) - except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as e: + except jwt.InvalidSignatureError as e: + logger.warning( + "Invalid signature. Is the Arcade Engine configured with the Actor secret '%s'?", + actor_secret, + ) + return TokenValidationResult(valid=False, error=str(e)) + + except jwt.InvalidTokenError as e: return TokenValidationResult(valid=False, error=str(e)) token_ver = payload.get("ver") diff --git a/arcade/arcade/actor/core/base.py b/arcade/arcade/actor/core/base.py index 4b5e6313..469abc4f 100644 --- a/arcade/arcade/actor/core/base.py +++ b/arcade/arcade/actor/core/base.py @@ -32,12 +32,13 @@ class BaseActor(Actor): HealthCheckComponent, ) - def __init__(self, disable_auth: bool = False) -> None: + def __init__(self, secret: str, disable_auth: bool = False) -> None: """ Initialize the BaseActor with an empty ToolCatalog. """ self.catalog = ToolCatalog() self.disable_auth = disable_auth + self.secret = secret def get_catalog(self) -> list[ToolDefinition]: """ diff --git a/arcade/arcade/actor/fastapi/actor.py b/arcade/arcade/actor/fastapi/actor.py index 5bb9621c..f4bca885 100644 --- a/arcade/arcade/actor/fastapi/actor.py +++ b/arcade/arcade/actor/fastapi/actor.py @@ -2,6 +2,7 @@ import json from typing import Any, Callable from fastapi import Depends, FastAPI, Request +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from arcade.actor.core.base import ( BaseActor, @@ -17,17 +18,20 @@ class FastAPIActor(BaseActor): An Arcade Actor that is hosted inside a FastAPI app. """ - def __init__(self, app: FastAPI, *, disable_auth: bool = False) -> None: + def __init__(self, app: FastAPI, *, secret: str, disable_auth: bool = False) -> None: """ Initialize the FastAPIActor with a FastAPI app instance and an empty ToolCatalog. """ - super().__init__(disable_auth) + super().__init__(secret, disable_auth) self.app = app self.router = FastAPIRouter(app, self) self.register_routes(self.router) +security = HTTPBearer() # Authorization: Bearer + + class FastAPIRouter(Router): def __init__(self, app: FastAPI, actor: BaseActor) -> None: self.app = app @@ -40,9 +44,19 @@ class FastAPIRouter(Router): use_auth_for_route = not self.actor.disable_auth and require_auth + def call_validate_engine_request(actor_secret: str) -> Callable: + async def dependency( + credentials: HTTPAuthorizationCredentials = Depends(security), + ) -> None: + await validate_engine_request(actor_secret, credentials) + + return dependency + async def wrapped_handler( request: Request, - _: None = Depends(validate_engine_request) if use_auth_for_route else None, + _: None = Depends(call_validate_engine_request(self.actor.secret)) + if use_auth_for_route + else None, ) -> Any: body_str = await request.body() body_json = json.loads(body_str) if body_str else {} diff --git a/arcade/arcade/actor/fastapi/auth.py b/arcade/arcade/actor/fastapi/auth.py index babda1e2..c17bbd5f 100644 --- a/arcade/arcade/actor/fastapi/auth.py +++ b/arcade/arcade/actor/fastapi/auth.py @@ -1,17 +1,16 @@ -from fastapi import Depends, HTTPException -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from fastapi import HTTPException +from fastapi.security import HTTPAuthorizationCredentials from arcade.actor.core.auth import validate_engine_token -security = HTTPBearer() # Authorization: Bearer - # Dependency function to validate JWT async def validate_engine_request( - credentials: HTTPAuthorizationCredentials = Depends(security), + actor_secret: str, + credentials: HTTPAuthorizationCredentials, ) -> None: jwt: str = credentials.credentials - validation_result = validate_engine_token(jwt) + validation_result = validate_engine_token(actor_secret, jwt) if not validation_result.valid: raise HTTPException( diff --git a/arcade/arcade/actor/flask/actor.py b/arcade/arcade/actor/flask/actor.py index c02f9428..81a5110c 100644 --- a/arcade/arcade/actor/flask/actor.py +++ b/arcade/arcade/actor/flask/actor.py @@ -14,12 +14,12 @@ class FlaskActor(BaseActor): An Arcade Actor that is hosted inside a Flask app. """ - def __init__(self, app: Flask) -> None: + def __init__(self, app: Flask, *, secret: str, disable_auth: bool = False) -> None: """ Initialize the FlaskActor with a Flask app instance and an empty ToolCatalog. """ - super().__init__() + super().__init__(secret, disable_auth) self.app = app self.router = FlaskRouter(app, self) self.register_routes(self.router) diff --git a/arcade/arcade/cli/serve.py b/arcade/arcade/cli/serve.py index 5a669a98..dd6c0610 100644 --- a/arcade/arcade/cli/serve.py +++ b/arcade/arcade/cli/serve.py @@ -1,3 +1,6 @@ +import logging +import os + from rich.console import Console try: @@ -15,6 +18,9 @@ except ImportError: from arcade.actor.fastapi.actor import FastAPIActor from arcade.core.toolkit import Toolkit +DEVELOPMENT_SECRET = "dev" # noqa: S105 + +logger = logging.getLogger(__name__) console = Console() @@ -24,28 +30,48 @@ def serve_default_actor( """ Get an instance of a FastAPI server with the Arcade Actor. """ + # Use Uvicorn's default log config for Arcade logging, + # to ensure a nice consistent style for all logs. + logging_config = uvicorn.config.LOGGING_CONFIG + logging_config["loggers"]["arcade"] = { + "handlers": ["default"], + "level": "INFO", + "propagate": False, + } + + # TODO: Pass in a logging config from the CLI, to set the log level. + logging.config.dictConfig(logging_config) + toolkits = Toolkit.find_all_arcade_toolkits() if not toolkits: - console.print("No toolkits found in Python environment. Exiting...", style="bold red") + logger.error("No toolkits found in Python environment. Exiting...") return else: - console.print("Serving the following toolkits:", style="bold blue") + logger.info("Serving the following toolkits:") for toolkit in toolkits: - console.print(f" - {toolkit.name} ({toolkit.package_name})") + logger.info(f" - {toolkit.name} ({toolkit.package_name})") + + actor_secret = os.environ.get("ARCADE_ACTOR_SECRET") + if not actor_secret: + logger.warning( + "Warning: ARCADE_ACTOR_SECRET environment variable is not set. Using 'dev' as the actor secret.", + ) + actor_secret = DEVELOPMENT_SECRET app = fastapi.FastAPI( title="Arcade AI Actor", description="Arcade AI default Actor implementation using FastAPI.", version="0.1.0", ) - actor = FastAPIActor(app, disable_auth=disable_auth) + actor = FastAPIActor(app, secret=actor_secret, disable_auth=disable_auth) for toolkit in toolkits: actor.register_toolkit(toolkit) - console.print("Starting FastAPI server...", style="bold blue") + logger.info("Starting FastAPI server...") uvicorn.run( app=app, host=host, port=port, + log_config=logging_config, )