bitrouter_runtime/
auth.rs1use std::sync::Arc;
22
23use sea_orm::DatabaseConnection;
24use sha2::{Digest, Sha256};
25use warp::Filter;
26
27use bitrouter_accounts::identity::{AccountId, Identity, Scope};
28use bitrouter_accounts::service::AccountService;
29
30#[derive(Clone)]
32pub struct AuthContext {
33 master_key_hash: Option<String>,
35 db: Option<DatabaseConnection>,
37}
38
39impl AuthContext {
40 pub fn new(master_key: Option<&str>, db: Option<DatabaseConnection>) -> Self {
41 Self {
42 master_key_hash: master_key.map(hash_key),
43 db,
44 }
45 }
46
47 pub fn is_open(&self) -> bool {
49 self.master_key_hash.is_none()
50 }
51}
52
53pub fn hash_key(key: &str) -> String {
55 let mut hasher = Sha256::new();
56 hasher.update(key.as_bytes());
57 hex::encode(hasher.finalize())
58}
59
60fn extract_bearer(header: &str) -> Option<&str> {
64 header
65 .strip_prefix("Bearer ")
66 .or_else(|| header.strip_prefix("bearer "))
67}
68
69pub fn bearer_credential() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
71 warp::header::optional::<String>("authorization").and_then(
72 |header: Option<String>| async move {
73 match header.and_then(|h| extract_bearer(&h).map(str::to_owned)) {
74 Some(key) => Ok(key),
75 None => Err(warp::reject::custom(Unauthorized("missing bearer token"))),
76 }
77 },
78 )
79}
80
81pub fn x_api_key_credential() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
83 warp::header::optional::<String>("x-api-key").and_then(|header: Option<String>| async move {
84 match header {
85 Some(key) if !key.is_empty() => Ok(key),
86 _ => Err(warp::reject::custom(Unauthorized("missing x-api-key"))),
87 }
88 })
89}
90
91pub fn any_credential() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
94 warp::header::optional::<String>("authorization")
95 .and(warp::header::optional::<String>("x-api-key"))
96 .and_then(
97 |auth_header: Option<String>, x_api_key: Option<String>| async move {
98 if let Some(key) = auth_header.and_then(|h| extract_bearer(&h).map(str::to_owned)) {
99 return Ok(key);
100 }
101 if let Some(key) = x_api_key.filter(|k| !k.is_empty()) {
102 return Ok(key);
103 }
104 Err(warp::reject::custom(Unauthorized(
105 "missing authentication credentials",
106 )))
107 },
108 )
109}
110
111async fn resolve_identity(
119 credential: &str,
120 ctx: &AuthContext,
121) -> Result<Identity, warp::Rejection> {
122 let credential_hash = hash_key(credential);
123
124 if let Some(ref master_hash) = ctx.master_key_hash
126 && constant_time_eq(&credential_hash, master_hash)
127 {
128 return Ok(Identity {
129 account_id: AccountId::new(),
130 scope: Scope::Admin,
131 });
132 }
133
134 if let Some(ref db) = ctx.db {
136 let svc = AccountService::new(db);
137 if let Ok(Some((account_id, _key))) = svc.resolve_api_key(&credential_hash).await {
138 return Ok(Identity {
139 account_id,
140 scope: Scope::Api,
141 });
142 }
143 }
144
145 Err(warp::reject::custom(Unauthorized("invalid API key")))
146}
147
148fn constant_time_eq(a: &str, b: &str) -> bool {
150 if a.len() != b.len() {
151 return false;
152 }
153 a.bytes()
154 .zip(b.bytes())
155 .fold(0u8, |acc, (x, y)| acc | (x ^ y))
156 == 0
157}
158
159pub fn openai_auth(
165 ctx: Arc<AuthContext>,
166) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
167 if ctx.is_open() {
168 return open_identity().boxed();
169 }
170 let ctx = ctx.clone();
171 bearer_credential()
172 .and(warp::any().map(move || ctx.clone()))
173 .and_then(|credential: String, ctx: Arc<AuthContext>| async move {
174 resolve_identity(&credential, &ctx).await
175 })
176 .boxed()
177}
178
179pub fn anthropic_auth(
183 ctx: Arc<AuthContext>,
184) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
185 if ctx.is_open() {
186 return open_identity().boxed();
187 }
188 let ctx = ctx.clone();
189 x_api_key_credential()
190 .and(warp::any().map(move || ctx.clone()))
191 .and_then(|credential: String, ctx: Arc<AuthContext>| async move {
192 resolve_identity(&credential, &ctx).await
193 })
194 .boxed()
195}
196
197pub fn management_auth(
201 ctx: Arc<AuthContext>,
202) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
203 if ctx.is_open() {
204 return open_identity().boxed();
205 }
206 let ctx = ctx.clone();
207 any_credential()
208 .and(warp::any().map(move || ctx.clone()))
209 .and_then(|credential: String, ctx: Arc<AuthContext>| async move {
210 resolve_identity(&credential, &ctx).await
211 })
212 .boxed()
213}
214
215fn open_identity() -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
217 warp::any().and_then(|| async {
218 Ok::<_, warp::Rejection>(Identity {
219 account_id: AccountId::new(),
220 scope: Scope::Admin,
221 })
222 })
223}
224
225#[derive(Debug)]
228pub struct Unauthorized(pub &'static str);
229
230impl std::fmt::Display for Unauthorized {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 write!(f, "unauthorized: {}", self.0)
233 }
234}
235
236impl warp::reject::Reject for Unauthorized {}