use std::{net::SocketAddr, sync::Arc};
use arrow_flight::flight_service_server::FlightServiceServer;
use tonic::{
Request, Status,
service::Interceptor,
transport::{Certificate, Identity, Server, ServerTlsConfig},
};
use crate::{
auth::{AuthConfig, Authorizer, BasicCredential},
error::Result,
registry::SchemaRegistry,
service::CypherFlightService,
};
#[derive(Debug, Clone, Default)]
pub struct ServerOptions {
pub tls: Option<TlsOptions>,
pub bearer_token: Option<String>,
pub bearer_tokens: Vec<String>,
pub basic_auth: Option<(String, String)>,
pub authorizer: Authorizer,
}
impl ServerOptions {
fn auth_config(&self) -> AuthConfig {
let mut cfg = AuthConfig::default().with_authorizer(self.authorizer.clone());
cfg.basic = self.basic_auth.as_ref().map(|(u, p)| BasicCredential {
username: u.clone(),
password: p.clone(),
});
if let Some(t) = &self.bearer_token {
cfg.add_bearer_token(t.clone());
}
for t in &self.bearer_tokens {
cfg.add_bearer_token(t.clone());
}
cfg
}
}
#[derive(Debug, Clone)]
pub struct TlsOptions {
pub cert_pem: Vec<u8>,
pub key_pem: Vec<u8>,
pub client_ca_pem: Option<Vec<u8>>,
}
impl TlsOptions {
pub fn from_pem_files(
cert: impl AsRef<std::path::Path>,
key: impl AsRef<std::path::Path>,
) -> std::io::Result<Self> {
Ok(Self {
cert_pem: std::fs::read(cert)?,
key_pem: std::fs::read(key)?,
client_ca_pem: None,
})
}
pub fn mutual_from_pem_files(
cert: impl AsRef<std::path::Path>,
key: impl AsRef<std::path::Path>,
client_ca: impl AsRef<std::path::Path>,
) -> std::io::Result<Self> {
Ok(Self {
cert_pem: std::fs::read(cert)?,
key_pem: std::fs::read(key)?,
client_ca_pem: Some(std::fs::read(client_ca)?),
})
}
}
#[derive(Clone)]
struct AuthInterceptor {
auth: Arc<AuthConfig>,
}
impl Interceptor for AuthInterceptor {
fn call(&mut self, mut request: Request<()>) -> std::result::Result<Request<()>, Status> {
let header = request
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok());
let identity = self.auth.authenticate(header)?;
request.extensions_mut().insert(identity);
Ok(request)
}
}
pub async fn run_server(
redis_url: &str,
graph: &str,
registry: Arc<SchemaRegistry>,
addr: SocketAddr,
) -> Result<()> {
run_server_with(redis_url, graph, registry, addr, ServerOptions::default()).await
}
pub async fn run_server_with(
redis_url: &str,
graph: &str,
registry: Arc<SchemaRegistry>,
addr: SocketAddr,
options: ServerOptions,
) -> Result<()> {
let auth = Arc::new(options.auth_config());
let service = CypherFlightService::connect(redis_url, graph, registry)
.await?
.with_auth(auth.clone());
let mut builder = Server::builder();
if let Some(tls) = &options.tls {
let identity = Identity::from_pem(&tls.cert_pem, &tls.key_pem);
let mut tls_config = ServerTlsConfig::new().identity(identity);
if let Some(ca) = &tls.client_ca_pem {
tls_config = tls_config.client_ca_root(Certificate::from_pem(ca));
}
builder = builder.tls_config(tls_config)?;
}
let router = if auth.is_enabled() {
let interceptor = AuthInterceptor { auth };
builder.add_service(FlightServiceServer::with_interceptor(service, interceptor))
} else {
builder.add_service(FlightServiceServer::new(service))
};
router.serve(addr).await?;
Ok(())
}
#[cfg(test)]
mod auth_tests {
use super::*;
use crate::auth::Identity;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
fn request_with_auth(value: Option<&str>) -> Request<()> {
let mut req = Request::new(());
if let Some(v) = value {
req.metadata_mut()
.insert("authorization", v.parse().unwrap());
}
req
}
fn interceptor(bearer: Option<&str>, basic: Option<(&str, &str)>) -> AuthInterceptor {
let opts = ServerOptions {
bearer_token: bearer.map(Into::into),
basic_auth: basic.map(|(u, p)| (u.into(), p.into())),
..ServerOptions::default()
};
AuthInterceptor {
auth: Arc::new(opts.auth_config()),
}
}
#[test]
fn bearer_accepts_correct_and_rejects_wrong() {
let mut auth = interceptor(Some("s3cret"), None);
assert!(auth.call(request_with_auth(Some("Bearer s3cret"))).is_ok());
let err = auth
.call(request_with_auth(Some("Bearer nope")))
.unwrap_err();
assert_eq!(err.code(), tonic::Code::Unauthenticated);
assert!(auth.call(request_with_auth(None)).is_err());
}
#[test]
fn basic_credentials_accepted() {
let mut auth = interceptor(None, Some(("analyst", "p@ss")));
let header = format!("Basic {}", BASE64.encode("analyst:p@ss"));
assert!(auth.call(request_with_auth(Some(&header))).is_ok());
let wrong = format!("Basic {}", BASE64.encode("analyst:nope"));
assert!(auth.call(request_with_auth(Some(&wrong))).is_err());
}
#[test]
fn options_map_to_auth_config() {
let opts = ServerOptions {
bearer_token: Some("t".into()),
basic_auth: Some(("u".into(), "p".into())),
..ServerOptions::default()
};
let cfg = opts.auth_config();
assert_eq!(cfg.issued_token(), Some("t"));
assert_eq!(cfg.basic.as_ref().map(|b| b.username.as_str()), Some("u"));
assert!(cfg.is_enabled());
}
#[test]
fn options_token_set_maps_to_rotating_config() {
let opts = ServerOptions {
bearer_token: Some("primary".into()),
bearer_tokens: vec!["secondary".into()],
..ServerOptions::default()
};
let cfg = opts.auth_config();
assert_eq!(cfg.issued_token(), Some("primary"));
assert!(cfg.check_header(Some("Bearer primary")).is_ok());
assert!(cfg.check_header(Some("Bearer secondary")).is_ok());
assert!(cfg.check_header(Some("Bearer stale")).is_err());
}
#[test]
fn rotation_accepts_old_and_new_rejects_stale_through_interceptor() {
let opts = ServerOptions {
bearer_tokens: vec!["old".into(), "new".into()],
..ServerOptions::default()
};
let mut auth = AuthInterceptor {
auth: Arc::new(opts.auth_config()),
};
assert!(auth.call(request_with_auth(Some("Bearer old"))).is_ok());
assert!(auth.call(request_with_auth(Some("Bearer new"))).is_ok());
assert!(
auth.call(request_with_auth(Some("Bearer retired")))
.is_err()
);
}
#[test]
fn interceptor_stashes_identity_extension() {
let opts = ServerOptions {
bearer_tokens: vec!["tok".into()],
..ServerOptions::default()
};
let mut auth = AuthInterceptor {
auth: Arc::new(opts.auth_config()),
};
let req = auth.call(request_with_auth(Some("Bearer tok"))).unwrap();
assert_eq!(
req.extensions().get::<Identity>(),
Some(&Identity::Token("tok".into())),
);
}
#[test]
fn options_carry_authorizer_into_config() {
let opts = ServerOptions {
bearer_tokens: vec!["tok".into()],
authorizer: Authorizer::allow_list([("tok", vec!["q1"])]),
..ServerOptions::default()
};
let cfg = opts.auth_config();
let id = Identity::Token("tok".into());
assert!(cfg.authorize(&id, "q1").is_ok());
let denied = cfg.authorize(&id, "q2").unwrap_err();
assert_eq!(denied.code(), tonic::Code::PermissionDenied);
}
}