1use std::sync::Arc;
22
23use chrono::Utc;
24use futu_auth::{KeyRecord, KeyStore, Scope};
25use tonic::{Request, Status};
26
27#[inline]
33pub fn scope_for_proto(proto_id: u32) -> Option<Scope> {
34 futu_auth::scope_for_proto_id(proto_id)
35}
36
37#[allow(clippy::result_large_err)] pub fn authenticate<T>(
43 store: &Arc<KeyStore>,
44 req: &Request<T>,
45) -> Result<Option<Arc<KeyRecord>>, Status> {
46 if !store.is_configured() {
47 return Ok(None);
48 }
49
50 let token = req
51 .metadata()
52 .get("authorization")
53 .and_then(|v| v.to_str().ok())
54 .and_then(|v| v.strip_prefix("Bearer ").map(|s| s.trim().to_string()));
55
56 let Some(token) = token else {
57 futu_auth::audit::reject(
58 "grpc",
59 "auth",
60 "<missing>",
61 "missing authorization: Bearer metadata",
62 );
63 return Err(Status::unauthenticated(
64 "missing authorization: Bearer <api-key> metadata",
65 ));
66 };
67
68 let Some(rec) = store.verify(&token) else {
69 futu_auth::audit::reject("grpc", "auth", "<invalid>", "invalid api key");
70 return Err(Status::unauthenticated("invalid API key"));
71 };
72
73 if rec.is_expired(Utc::now()) {
74 futu_auth::audit::reject("grpc", "auth", &rec.id, "key expired");
75 return Err(Status::unauthenticated(format!(
76 "API key {:?} expired",
77 rec.id
78 )));
79 }
80
81 Ok(Some(rec))
82}
83
84#[allow(clippy::result_large_err)] pub fn check_scope(
87 rec: &Option<Arc<KeyRecord>>,
88 proto_id: u32,
89 needed: Scope,
90) -> Result<(), Status> {
91 let Some(rec) = rec else {
92 return Ok(());
94 };
95 let endpoint = format!("proto_id={}", proto_id);
96 if !rec.scopes.contains(&needed) {
97 futu_auth::audit::reject(
98 "grpc",
99 &endpoint,
100 &rec.id,
101 &format!("missing scope {}", needed),
102 );
103 return Err(Status::permission_denied(format!(
104 "API key {:?} missing scope {:?} for proto_id={}",
105 rec.id,
106 needed.as_str(),
107 proto_id
108 )));
109 }
110 futu_auth::audit::allow("grpc", &endpoint, &rec.id, Some(needed.as_str()));
111 Ok(())
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn proto_range_mapping() {
120 assert_eq!(scope_for_proto(1002), None); assert_eq!(scope_for_proto(3004), Some(Scope::QotRead)); assert_eq!(scope_for_proto(3223), Some(Scope::QotRead)); assert_eq!(scope_for_proto(2001), Some(Scope::AccRead)); assert_eq!(scope_for_proto(2101), Some(Scope::AccRead)); assert_eq!(scope_for_proto(2201), Some(Scope::AccRead)); assert_eq!(scope_for_proto(2005), Some(Scope::TradeReal)); assert_eq!(scope_for_proto(2202), Some(Scope::TradeReal)); assert_eq!(scope_for_proto(2205), Some(Scope::TradeReal)); }
130}