use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::debug;
use typesec_core::{
ResourceId, SubjectId,
policy::{PolicyEngine, PolicyResult},
};
use crate::http::HttpClient;
use crate::provider::{ProviderHttpEngine, ProviderHttpError};
#[derive(Debug, Clone, Serialize)]
pub struct ArcadeToolAuthRequest {
pub tool_name: String,
pub user_id: String,
}
#[derive(Debug, Deserialize)]
struct ArcadeToolAuthResponse {
status: String,
#[serde(default)]
url: Option<String>,
}
pub struct ArcadeToolAuthEngine {
engine: ProviderHttpEngine,
tool_map: HashMap<String, String>,
}
impl ArcadeToolAuthEngine {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
engine: ProviderHttpEngine::new(api_key, "https://api.arcade.dev"),
tool_map: HashMap::new(),
}
}
pub fn with_http(
api_key: impl Into<String>,
base_url: impl Into<String>,
http: Arc<dyn HttpClient>,
) -> Self {
Self {
engine: ProviderHttpEngine::with_http(api_key, base_url, http),
tool_map: HashMap::new(),
}
}
pub fn with_tool_mapping(
mut self,
resource: impl Into<String>,
tool: impl Into<String>,
) -> Self {
self.tool_map.insert(resource.into(), tool.into());
self
}
fn tool_name_for<'a>(&'a self, resource: &'a str) -> Option<&'a str> {
self.tool_map
.get(resource)
.map(String::as_str)
.or_else(|| resource.contains('.').then_some(resource))
}
}
impl PolicyEngine for ArcadeToolAuthEngine {
fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
let subject = subject.as_str();
let resource = resource.as_str();
debug!(subject, action, resource, "arcade tool authorization check");
if action != "execute" && action != "read" && action != "write" {
return PolicyResult::delegate(
"arcade",
format!("Arcade tool auth does not handle action '{action}'"),
);
}
let Some(tool_name) = self.tool_name_for(resource) else {
return PolicyResult::delegate(
"arcade",
format!("no Arcade tool mapping for resource '{resource}'"),
);
};
let url = format!("{}/v1/tools/authorize", self.engine.base_url());
let body = json!({
"tool_name": tool_name,
"user_id": subject,
});
match self
.engine
.bearer_post::<ArcadeToolAuthResponse>(&url, &body)
{
Ok(response) if response.status == "completed" => PolicyResult::Allow,
Ok(response) => {
let url = response
.url
.map(|url| format!("; authorize at {url}"))
.unwrap_or_default();
PolicyResult::Deny(format!(
"Arcade authorization for tool '{tool_name}' is '{}'{}",
response.status, url
))
}
Err(ProviderHttpError::Parse(err)) => {
PolicyResult::Deny(format!("Arcade response parse error: {err}"))
}
Err(ProviderHttpError::Transport(err)) => {
PolicyResult::Deny(format!("Arcade authorization check failed: {err}"))
}
}
}
}
#[cfg(test)]
mod tests;