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 dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from arcade.core.config import config
|
|
||||||
|
|
||||||
SUPPORTED_TOKEN_VER = "1" # noqa: S105 Possible hardcoded password assigned (false positive)
|
SUPPORTED_TOKEN_VER = "1" # noqa: S105 Possible hardcoded password assigned (false positive)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TokenValidationResult:
|
class TokenValidationResult:
|
||||||
|
|
@ -18,16 +19,23 @@ class SigningAlgorithm(str, Enum):
|
||||||
HS256 = "HS256"
|
HS256 = "HS256"
|
||||||
|
|
||||||
|
|
||||||
def validate_engine_token(token: str) -> TokenValidationResult:
|
def validate_engine_token(actor_secret: str, token: str) -> TokenValidationResult:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
config.api.key,
|
actor_secret,
|
||||||
algorithms=[SigningAlgorithm.HS256],
|
algorithms=[SigningAlgorithm.HS256],
|
||||||
verify=True,
|
verify=True,
|
||||||
audience="actor",
|
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))
|
return TokenValidationResult(valid=False, error=str(e))
|
||||||
|
|
||||||
token_ver = payload.get("ver")
|
token_ver = payload.get("ver")
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,13 @@ class BaseActor(Actor):
|
||||||
HealthCheckComponent,
|
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.
|
Initialize the BaseActor with an empty ToolCatalog.
|
||||||
"""
|
"""
|
||||||
self.catalog = ToolCatalog()
|
self.catalog = ToolCatalog()
|
||||||
self.disable_auth = disable_auth
|
self.disable_auth = disable_auth
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
def get_catalog(self) -> list[ToolDefinition]:
|
def get_catalog(self) -> list[ToolDefinition]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
from arcade.actor.core.base import (
|
from arcade.actor.core.base import (
|
||||||
BaseActor,
|
BaseActor,
|
||||||
|
|
@ -17,17 +18,20 @@ 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, *, disable_auth: bool = False) -> None:
|
def __init__(self, app: FastAPI, *, secret: str, 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__(disable_auth)
|
super().__init__(secret, 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)
|
||||||
|
|
||||||
|
|
||||||
|
security = HTTPBearer() # Authorization: Bearer <xxx>
|
||||||
|
|
||||||
|
|
||||||
class FastAPIRouter(Router):
|
class FastAPIRouter(Router):
|
||||||
def __init__(self, app: FastAPI, actor: BaseActor) -> None:
|
def __init__(self, app: FastAPI, actor: BaseActor) -> None:
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
@ -40,9 +44,19 @@ class FastAPIRouter(Router):
|
||||||
|
|
||||||
use_auth_for_route = not self.actor.disable_auth and require_auth
|
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(
|
async def wrapped_handler(
|
||||||
request: Request,
|
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:
|
) -> 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 {}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials
|
||||||
|
|
||||||
from arcade.actor.core.auth import validate_engine_token
|
from arcade.actor.core.auth import validate_engine_token
|
||||||
|
|
||||||
security = HTTPBearer() # Authorization: Bearer <xxx>
|
|
||||||
|
|
||||||
|
|
||||||
# Dependency function to validate JWT
|
# Dependency function to validate JWT
|
||||||
async def validate_engine_request(
|
async def validate_engine_request(
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
actor_secret: str,
|
||||||
|
credentials: HTTPAuthorizationCredentials,
|
||||||
) -> None:
|
) -> None:
|
||||||
jwt: str = credentials.credentials
|
jwt: str = credentials.credentials
|
||||||
validation_result = validate_engine_token(jwt)
|
validation_result = validate_engine_token(actor_secret, jwt)
|
||||||
|
|
||||||
if not validation_result.valid:
|
if not validation_result.valid:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ class FlaskActor(BaseActor):
|
||||||
An Arcade Actor that is hosted inside a Flask app.
|
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
|
Initialize the FlaskActor with a Flask app
|
||||||
instance and an empty ToolCatalog.
|
instance and an empty ToolCatalog.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__(secret, disable_auth)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.router = FlaskRouter(app, self)
|
self.router = FlaskRouter(app, self)
|
||||||
self.register_routes(self.router)
|
self.register_routes(self.router)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -15,6 +18,9 @@ except ImportError:
|
||||||
from arcade.actor.fastapi.actor import FastAPIActor
|
from arcade.actor.fastapi.actor import FastAPIActor
|
||||||
from arcade.core.toolkit import Toolkit
|
from arcade.core.toolkit import Toolkit
|
||||||
|
|
||||||
|
DEVELOPMENT_SECRET = "dev" # noqa: S105
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,28 +30,48 @@ def serve_default_actor(
|
||||||
"""
|
"""
|
||||||
Get an instance of a FastAPI server with the Arcade 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()
|
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||||
if not 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
|
return
|
||||||
else:
|
else:
|
||||||
console.print("Serving the following toolkits:", style="bold blue")
|
logger.info("Serving the following toolkits:")
|
||||||
for toolkit in 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(
|
app = fastapi.FastAPI(
|
||||||
title="Arcade AI Actor",
|
title="Arcade AI Actor",
|
||||||
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, disable_auth=disable_auth)
|
actor = FastAPIActor(app, secret=actor_secret, disable_auth=disable_auth)
|
||||||
for toolkit in toolkits:
|
for toolkit in toolkits:
|
||||||
actor.register_toolkit(toolkit)
|
actor.register_toolkit(toolkit)
|
||||||
|
|
||||||
console.print("Starting FastAPI server...", style="bold blue")
|
logger.info("Starting FastAPI server...")
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app=app,
|
app=app,
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
log_config=logging_config,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue