Actor: Use shared secret for Actor auth (#40)

Actor side of https://github.com/ArcadeAI/Engine/pull/78 (see comments
there)
This commit is contained in:
Nate Barbettini 2024-09-17 17:03:40 -07:00 committed by GitHub
parent f4fe8c7892
commit e4839195d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 70 additions and 22 deletions

View file

@ -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")

View file

@ -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]:
"""

View file

@ -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 <xxx>
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 {}

View file

@ -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 <xxx>
# 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(

View file

@ -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)

View file

@ -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,
)