1use std::{net::SocketAddr, sync::Arc};
2
3use arrow_flight::flight_service_server::FlightServiceServer;
4use tonic::{
5 Request, Status,
6 service::Interceptor,
7 transport::{Certificate, Identity, Server, ServerTlsConfig},
8};
9
10use crate::{
11 auth::{AuthConfig, Authorizer, BasicCredential},
12 error::Result,
13 registry::SchemaRegistry,
14 service::CypherFlightService,
15};
16
17#[derive(Debug, Clone, Default)]
22pub struct ServerOptions {
23 pub tls: Option<TlsOptions>,
26 pub bearer_token: Option<String>,
32 pub bearer_tokens: Vec<String>,
39 pub basic_auth: Option<(String, String)>,
43 pub authorizer: Authorizer,
46}
47
48impl ServerOptions {
49 fn auth_config(&self) -> AuthConfig {
50 let mut cfg = AuthConfig::default().with_authorizer(self.authorizer.clone());
51 cfg.basic = self.basic_auth.as_ref().map(|(u, p)| BasicCredential {
52 username: u.clone(),
53 password: p.clone(),
54 });
55 if let Some(t) = &self.bearer_token {
57 cfg.add_bearer_token(t.clone());
58 }
59 for t in &self.bearer_tokens {
60 cfg.add_bearer_token(t.clone());
61 }
62 cfg
63 }
64}
65
66#[derive(Debug, Clone)]
67pub struct TlsOptions {
68 pub cert_pem: Vec<u8>,
69 pub key_pem: Vec<u8>,
70 pub client_ca_pem: Option<Vec<u8>>,
74}
75
76impl TlsOptions {
77 pub fn from_pem_files(
79 cert: impl AsRef<std::path::Path>,
80 key: impl AsRef<std::path::Path>,
81 ) -> std::io::Result<Self> {
82 Ok(Self {
83 cert_pem: std::fs::read(cert)?,
84 key_pem: std::fs::read(key)?,
85 client_ca_pem: None,
86 })
87 }
88
89 pub fn mutual_from_pem_files(
92 cert: impl AsRef<std::path::Path>,
93 key: impl AsRef<std::path::Path>,
94 client_ca: impl AsRef<std::path::Path>,
95 ) -> std::io::Result<Self> {
96 Ok(Self {
97 cert_pem: std::fs::read(cert)?,
98 key_pem: std::fs::read(key)?,
99 client_ca_pem: Some(std::fs::read(client_ca)?),
100 })
101 }
102}
103
104#[derive(Clone)]
109struct AuthInterceptor {
110 auth: Arc<AuthConfig>,
111}
112
113impl Interceptor for AuthInterceptor {
114 fn call(&mut self, mut request: Request<()>) -> std::result::Result<Request<()>, Status> {
115 let header = request
116 .metadata()
117 .get("authorization")
118 .and_then(|v| v.to_str().ok());
119 let identity = self.auth.authenticate(header)?;
122 request.extensions_mut().insert(identity);
123 Ok(request)
124 }
125}
126
127pub async fn run_server(
148 redis_url: &str,
149 graph: &str,
150 registry: Arc<SchemaRegistry>,
151 addr: SocketAddr,
152) -> Result<()> {
153 run_server_with(redis_url, graph, registry, addr, ServerOptions::default()).await
154}
155
156pub async fn run_server_with(
187 redis_url: &str,
188 graph: &str,
189 registry: Arc<SchemaRegistry>,
190 addr: SocketAddr,
191 options: ServerOptions,
192) -> Result<()> {
193 let auth = Arc::new(options.auth_config());
194 let service = CypherFlightService::connect(redis_url, graph, registry)
195 .await?
196 .with_auth(auth.clone());
197
198 let mut builder = Server::builder();
199 if let Some(tls) = &options.tls {
200 let identity = Identity::from_pem(&tls.cert_pem, &tls.key_pem);
201 let mut tls_config = ServerTlsConfig::new().identity(identity);
202 if let Some(ca) = &tls.client_ca_pem {
203 tls_config = tls_config.client_ca_root(Certificate::from_pem(ca));
205 }
206 builder = builder.tls_config(tls_config)?;
207 }
208
209 let router = if auth.is_enabled() {
212 let interceptor = AuthInterceptor { auth };
213 builder.add_service(FlightServiceServer::with_interceptor(service, interceptor))
214 } else {
215 builder.add_service(FlightServiceServer::new(service))
216 };
217
218 router.serve(addr).await?;
219 Ok(())
220}
221
222#[cfg(test)]
223mod auth_tests {
224 use super::*;
225 use crate::auth::Identity;
226 use base64::Engine;
227 use base64::engine::general_purpose::STANDARD as BASE64;
228
229 fn request_with_auth(value: Option<&str>) -> Request<()> {
230 let mut req = Request::new(());
231 if let Some(v) = value {
232 req.metadata_mut()
233 .insert("authorization", v.parse().unwrap());
234 }
235 req
236 }
237
238 fn interceptor(bearer: Option<&str>, basic: Option<(&str, &str)>) -> AuthInterceptor {
239 let opts = ServerOptions {
240 bearer_token: bearer.map(Into::into),
241 basic_auth: basic.map(|(u, p)| (u.into(), p.into())),
242 ..ServerOptions::default()
243 };
244 AuthInterceptor {
245 auth: Arc::new(opts.auth_config()),
246 }
247 }
248
249 #[test]
250 fn bearer_accepts_correct_and_rejects_wrong() {
251 let mut auth = interceptor(Some("s3cret"), None);
252 assert!(auth.call(request_with_auth(Some("Bearer s3cret"))).is_ok());
253 let err = auth
254 .call(request_with_auth(Some("Bearer nope")))
255 .unwrap_err();
256 assert_eq!(err.code(), tonic::Code::Unauthenticated);
257 assert!(auth.call(request_with_auth(None)).is_err());
258 }
259
260 #[test]
261 fn basic_credentials_accepted() {
262 let mut auth = interceptor(None, Some(("analyst", "p@ss")));
263 let header = format!("Basic {}", BASE64.encode("analyst:p@ss"));
264 assert!(auth.call(request_with_auth(Some(&header))).is_ok());
265 let wrong = format!("Basic {}", BASE64.encode("analyst:nope"));
266 assert!(auth.call(request_with_auth(Some(&wrong))).is_err());
267 }
268
269 #[test]
270 fn options_map_to_auth_config() {
271 let opts = ServerOptions {
272 bearer_token: Some("t".into()),
273 basic_auth: Some(("u".into(), "p".into())),
274 ..ServerOptions::default()
275 };
276 let cfg = opts.auth_config();
277 assert_eq!(cfg.issued_token(), Some("t"));
278 assert_eq!(cfg.basic.as_ref().map(|b| b.username.as_str()), Some("u"));
279 assert!(cfg.is_enabled());
280 }
281
282 #[test]
283 fn options_token_set_maps_to_rotating_config() {
284 let opts = ServerOptions {
287 bearer_token: Some("primary".into()),
288 bearer_tokens: vec!["secondary".into()],
289 ..ServerOptions::default()
290 };
291 let cfg = opts.auth_config();
292 assert_eq!(cfg.issued_token(), Some("primary"));
293 assert!(cfg.check_header(Some("Bearer primary")).is_ok());
294 assert!(cfg.check_header(Some("Bearer secondary")).is_ok());
295 assert!(cfg.check_header(Some("Bearer stale")).is_err());
296 }
297
298 #[test]
299 fn rotation_accepts_old_and_new_rejects_stale_through_interceptor() {
300 let opts = ServerOptions {
301 bearer_tokens: vec!["old".into(), "new".into()],
302 ..ServerOptions::default()
303 };
304 let mut auth = AuthInterceptor {
305 auth: Arc::new(opts.auth_config()),
306 };
307 assert!(auth.call(request_with_auth(Some("Bearer old"))).is_ok());
308 assert!(auth.call(request_with_auth(Some("Bearer new"))).is_ok());
309 assert!(
310 auth.call(request_with_auth(Some("Bearer retired")))
311 .is_err()
312 );
313 }
314
315 #[test]
316 fn interceptor_stashes_identity_extension() {
317 let opts = ServerOptions {
318 bearer_tokens: vec!["tok".into()],
319 ..ServerOptions::default()
320 };
321 let mut auth = AuthInterceptor {
322 auth: Arc::new(opts.auth_config()),
323 };
324 let req = auth.call(request_with_auth(Some("Bearer tok"))).unwrap();
325 assert_eq!(
326 req.extensions().get::<Identity>(),
327 Some(&Identity::Token("tok".into())),
328 );
329 }
330
331 #[test]
332 fn options_carry_authorizer_into_config() {
333 let opts = ServerOptions {
335 bearer_tokens: vec!["tok".into()],
336 authorizer: Authorizer::allow_list([("tok", vec!["q1"])]),
337 ..ServerOptions::default()
338 };
339 let cfg = opts.auth_config();
340 let id = Identity::Token("tok".into());
341 assert!(cfg.authorize(&id, "q1").is_ok());
342 let denied = cfg.authorize(&id, "q2").unwrap_err();
343 assert_eq!(denied.code(), tonic::Code::PermissionDenied);
344 }
345}