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:
parent
f4fe8c7892
commit
e4839195d7
6 changed files with 70 additions and 22 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue