1use std::collections::{BTreeMap, BTreeSet, HashMap};
2
3use async_trait::async_trait;
4use axum::extract::{FromRef, FromRequestParts, Path};
5use axum::http::HeaderMap;
6use axum::http::request::Parts;
7use fslite_core::{Capability, RequestContext, WorkspaceId};
8use serde_json::Value;
9
10use crate::error::ApiError;
11use crate::state::AppState;
12
13#[derive(Clone, Debug)]
15pub struct AuthenticatedActor {
16 pub workspace_id: WorkspaceId,
18 pub capabilities: BTreeSet<Capability>,
20 pub actor_metadata: BTreeMap<String, Value>,
22}
23
24#[async_trait]
31pub trait AuthProvider: Send + Sync {
32 async fn authenticate(&self, headers: &HeaderMap) -> Result<AuthenticatedActor, ApiError>;
34}
35
36pub struct BearerTokenAuthProvider {
38 tokens: HashMap<String, AuthenticatedActor>,
39}
40
41impl BearerTokenAuthProvider {
42 pub fn new(tokens: HashMap<String, AuthenticatedActor>) -> Self {
44 Self { tokens }
45 }
46}
47
48#[async_trait]
49impl AuthProvider for BearerTokenAuthProvider {
50 async fn authenticate(&self, headers: &HeaderMap) -> Result<AuthenticatedActor, ApiError> {
51 let header = headers
52 .get(axum::http::header::AUTHORIZATION)
53 .and_then(|value| value.to_str().ok())
54 .ok_or_else(|| ApiError::Unauthenticated("missing authorization header".into()))?;
55
56 let token = header
57 .strip_prefix("Bearer ")
58 .ok_or_else(|| ApiError::Unauthenticated("expected a Bearer token".into()))?;
59
60 self.tokens
61 .get(token)
62 .cloned()
63 .ok_or_else(|| ApiError::Unauthenticated("unrecognized token".into()))
64 }
65}
66
67pub struct Ctx(pub RequestContext);
70
71impl<S> FromRequestParts<S> for Ctx
72where
73 AppState: FromRef<S>,
74 S: Send + Sync,
75{
76 type Rejection = ApiError;
77
78 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
79 let app_state = AppState::from_ref(state);
80 let actor = app_state.auth.authenticate(&parts.headers).await?;
81
82 let Path(raw_params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
88 .await
89 .map_err(|_| ApiError::MalformedBody("invalid path parameters".into()))?;
90 let raw_workspace_id = raw_params
91 .get("workspace_id")
92 .ok_or_else(|| ApiError::MalformedBody("missing workspace id in path".into()))?;
93 let workspace_id = WorkspaceId::parse(raw_workspace_id)
94 .map_err(|_| ApiError::MalformedBody("invalid workspace id in path".into()))?;
95
96 if actor.workspace_id != workspace_id {
97 return Err(ApiError::WorkspaceMismatch);
98 }
99
100 Ok(Ctx(RequestContext::new(
101 workspace_id,
102 actor.actor_metadata,
103 actor.capabilities,
104 )))
105 }
106}