Override Actor auth for easier dev testing (#33)

In this PR:
- The Actor health check route now _never_ requires auth (bearer token).
It is always unprotected.
- `arcade dev --no-auth` disables all Actor auth entirely, making all
routes unprotected. Useful for debugging, but emits a warning to the
console.
This commit is contained in:
Nate Barbettini 2024-09-10 09:39:12 -07:00 committed by GitHub
parent 75c6a2becf
commit c59bb678dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 42 additions and 13 deletions

View file

@ -32,11 +32,12 @@ class BaseActor(Actor):
HealthCheckComponent, HealthCheckComponent,
) )
def __init__(self) -> None: def __init__(self, disable_auth: bool = False) -> None:
""" """
Initialize the BaseActor with an empty ToolCatalog. Initialize the BaseActor with an empty ToolCatalog.
""" """
self.catalog = ToolCatalog() self.catalog = ToolCatalog()
self.disable_auth = disable_auth
def get_catalog(self) -> list[ToolDefinition]: def get_catalog(self) -> list[ToolDefinition]:
""" """

View file

@ -27,7 +27,9 @@ class Router(ABC):
""" """
@abstractmethod @abstractmethod
def add_route(self, endpoint_path: str, handler: Callable, method: str) -> None: def add_route(
self, endpoint_path: str, handler: Callable, method: str, require_auth: bool = True
) -> None:
""" """
Add a route to the router. Add a route to the router.
""" """

View file

@ -48,7 +48,7 @@ class HealthCheckComponent(ActorComponent):
""" """
Register the health check route with the router. Register the health check route with the router.
""" """
router.add_route("health", self, method="GET") router.add_route("health", self, method="GET", require_auth=False)
async def __call__(self, request: RequestData) -> dict[str, Any]: async def __call__(self, request: RequestData) -> dict[str, Any]:
""" """

View file

@ -17,12 +17,12 @@ class FastAPIActor(BaseActor):
An Arcade Actor that is hosted inside a FastAPI app. An Arcade Actor that is hosted inside a FastAPI app.
""" """
def __init__(self, app: FastAPI) -> None: def __init__(self, app: FastAPI, *, disable_auth: bool = False) -> None:
""" """
Initialize the FastAPIActor with a FastAPI app Initialize the FastAPIActor with a FastAPI app
instance and an empty ToolCatalog. instance and an empty ToolCatalog.
""" """
super().__init__() super().__init__(disable_auth)
self.app = app self.app = app
self.router = FastAPIRouter(app, self) self.router = FastAPIRouter(app, self)
self.register_routes(self.router) self.register_routes(self.router)
@ -33,14 +33,16 @@ class FastAPIRouter(Router):
self.app = app self.app = app
self.actor = actor self.actor = actor
def _wrap_handler(self, handler: Callable) -> Callable: def _wrap_handler(self, handler: Callable, require_auth: bool = True) -> Callable:
""" """
Wrap the handler to handle FastAPI-specific request and response. Wrap the handler to handle FastAPI-specific request and response.
""" """
use_auth_for_route = not self.actor.disable_auth and require_auth
async def wrapped_handler( async def wrapped_handler(
request: Request, request: Request,
_: None = Depends(validate_engine_request), _: None = Depends(validate_engine_request) if use_auth_for_route else None,
) -> Any: ) -> Any:
body_str = await request.body() body_str = await request.body()
body_json = json.loads(body_str) if body_str else {} body_json = json.loads(body_str) if body_str else {}
@ -56,10 +58,14 @@ class FastAPIRouter(Router):
return wrapped_handler return wrapped_handler
def add_route(self, endpoint_path: str, handler: Callable, method: str) -> None: def add_route(
self, endpoint_path: str, handler: Callable, method: str, require_auth: bool = True
) -> None:
""" """
Add a route to the FastAPI application. Add a route to the FastAPI application.
""" """
self.app.add_api_route( self.app.add_api_route(
f"{self.actor.base_path}/{endpoint_path}", self._wrap_handler(handler), methods=[method] f"{self.actor.base_path}/{endpoint_path}",
self._wrap_handler(handler, require_auth),
methods=[method],
) )

View file

@ -56,10 +56,15 @@ class FlaskRouter(Router):
return wrapped_handler return wrapped_handler
def add_route(self, endpoint_path: str, handler: Callable, method: str) -> None: def add_route(
self, endpoint_path: str, handler: Callable, method: str, require_auth: bool = True
) -> None:
""" """
Add a route to the Flask application. Add a route to the Flask application.
""" """
# TODO: Implement auth
# use_auth_for_route = not self.actor.disable_auth and require_auth
handler_name = handler.__name__ if hasattr(handler, "__name__") else type(handler).__name__ handler_name = handler.__name__ if hasattr(handler, "__name__") else type(handler).__name__
endpoint_name = f"actor_{handler_name}_{method}" endpoint_name = f"actor_{handler_name}_{method}"
self.app.add_url_rule( self.app.add_url_rule(

View file

@ -263,15 +263,28 @@ def dev(
port: int = typer.Option( port: int = typer.Option(
"8000", "-p", "--port", help="Port for the app, defaults to ", show_default=True "8000", "-p", "--port", help="Port for the app, defaults to ", show_default=True
), ),
disable_auth: bool = typer.Option(
False,
"--no-auth",
help="Disable authentication for the actor. Not recommended for production.",
show_default=True,
),
) -> None: ) -> None:
""" """
Starts the actor with host, port, and reload options. Uses Starts the actor with host, port, and reload options. Uses
Uvicorn as ASGI actor. Parameters allow runtime configuration. Uvicorn as ASGI actor. Parameters allow runtime configuration.
""" """
if disable_auth:
console.print(
"⚠️ Actor authentication is disabled. Not recommended for production.",
style="bold yellow",
)
from arcade.cli.serve import serve_default_actor from arcade.cli.serve import serve_default_actor
try: try:
serve_default_actor(host, port) serve_default_actor(host, port, disable_auth)
except KeyboardInterrupt: except KeyboardInterrupt:
console.print("actor stopped by user.", style="bold red") console.print("actor stopped by user.", style="bold red")
typer.Exit() typer.Exit()

View file

@ -18,7 +18,9 @@ from arcade.core.toolkit import Toolkit
console = Console() console = Console()
def serve_default_actor(host: str = "127.0.0.1", port: int = 8000) -> None: def serve_default_actor(
host: str = "127.0.0.1", port: int = 8000, disable_auth: bool = False
) -> None:
""" """
Get an instance of a FastAPI server with the Arcade Actor. Get an instance of a FastAPI server with the Arcade Actor.
""" """
@ -36,7 +38,7 @@ def serve_default_actor(host: str = "127.0.0.1", port: int = 8000) -> None:
description="Arcade AI default Actor implementation using FastAPI.", description="Arcade AI default Actor implementation using FastAPI.",
version="0.1.0", version="0.1.0",
) )
actor = FastAPIActor(app) actor = FastAPIActor(app, disable_auth=disable_auth)
for toolkit in toolkits: for toolkit in toolkits:
actor.register_toolkit(toolkit) actor.register_toolkit(toolkit)