use std::collections::{BTreeMap, BTreeSet, HashMap};
use async_trait::async_trait;
use axum::extract::{FromRef, FromRequestParts, Path};
use axum::http::HeaderMap;
use axum::http::request::Parts;
use fslite_core::{Capability, RequestContext, WorkspaceId};
use serde_json::Value;
use crate::error::ApiError;
use crate::state::AppState;
#[derive(Clone, Debug)]
pub struct AuthenticatedActor {
pub workspace_id: WorkspaceId,
pub capabilities: BTreeSet<Capability>,
pub actor_metadata: BTreeMap<String, Value>,
}
#[async_trait]
pub trait AuthProvider: Send + Sync {
async fn authenticate(&self, headers: &HeaderMap) -> Result<AuthenticatedActor, ApiError>;
}
pub struct BearerTokenAuthProvider {
tokens: HashMap<String, AuthenticatedActor>,
}
impl BearerTokenAuthProvider {
pub fn new(tokens: HashMap<String, AuthenticatedActor>) -> Self {
Self { tokens }
}
}
#[async_trait]
impl AuthProvider for BearerTokenAuthProvider {
async fn authenticate(&self, headers: &HeaderMap) -> Result<AuthenticatedActor, ApiError> {
let header = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| ApiError::Unauthenticated("missing authorization header".into()))?;
let token = header
.strip_prefix("Bearer ")
.ok_or_else(|| ApiError::Unauthenticated("expected a Bearer token".into()))?;
self.tokens
.get(token)
.cloned()
.ok_or_else(|| ApiError::Unauthenticated("unrecognized token".into()))
}
}
pub struct Ctx(pub RequestContext);
impl<S> FromRequestParts<S> for Ctx
where
AppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state = AppState::from_ref(state);
let actor = app_state.auth.authenticate(&parts.headers).await?;
let Path(raw_params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
.await
.map_err(|_| ApiError::MalformedBody("invalid path parameters".into()))?;
let raw_workspace_id = raw_params
.get("workspace_id")
.ok_or_else(|| ApiError::MalformedBody("missing workspace id in path".into()))?;
let workspace_id = WorkspaceId::parse(raw_workspace_id)
.map_err(|_| ApiError::MalformedBody("invalid workspace id in path".into()))?;
if actor.workspace_id != workspace_id {
return Err(ApiError::WorkspaceMismatch);
}
Ok(Ctx(RequestContext::new(
workspace_id,
actor.actor_metadata,
actor.capabilities,
)))
}
}