use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock, RwLock};
use std::task::{Context, Poll};
use tonic::body::BoxBody;
use tonic::codegen::http;
use tower::Service;
use crate::runtime::authn::PostgresApiKeyStore;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VerifiedPrincipal {
pub credential_type: i32,
pub subject: String,
pub service_identity: String,
pub tenant_id: String,
pub project_id: String,
pub scopes: Vec<String>,
pub roles: Vec<String>,
pub credential_id: String,
pub auth_method: String,
pub certificate_identity: Option<String>,
}
impl VerifiedPrincipal {
pub(crate) fn from_verified_bearer_claims(
claims: &crate::runtime::security::SecurityClaims,
) -> Self {
let service_identity = claims.service_identity.clone().unwrap_or_default();
let auth_method = claims
.auth_method
.clone()
.unwrap_or_else(|| "bearer_jwt".to_string());
let credential_type = if !service_identity.trim().is_empty() {
4 } else {
match auth_method.trim().to_ascii_lowercase().as_str() {
"session" => 2,
"api_key" | "apikey" => 3,
"mtls" => 5,
_ => 1, }
};
Self {
credential_type,
subject: claims.sub.clone().unwrap_or_default(),
service_identity,
tenant_id: claims.tenant_id.clone().unwrap_or_default(),
project_id: claims.project_id.clone().unwrap_or_default(),
scopes: claims.resolved_scopes(),
roles: claims.roles.clone().unwrap_or_default(),
credential_id: claims.jti.clone().unwrap_or_default(),
auth_method,
certificate_identity: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PreresolvedCredentials {
pub bearer: Option<Result<VerifiedPrincipal, String>>,
pub api_key: Option<Result<Option<VerifiedPrincipal>, String>>,
pub certificate_identity: Option<String>,
pub certificate_present: bool,
pub certificate_principal: Option<VerifiedPrincipal>,
pub certificate_resolution_failed: bool,
pub certificate_resolution_unavailable: bool,
}
pub struct AuthPlaneDeps {
pub pool: sqlx::PgPool,
pub api_key_store: Arc<PostgresApiKeyStore>,
pub api_key_hash_key: Vec<u8>,
}
static AUTH_PLANE_DEPS: OnceLock<RwLock<Option<Arc<AuthPlaneDeps>>>> = OnceLock::new();
fn auth_plane_deps_cell() -> &'static RwLock<Option<Arc<AuthPlaneDeps>>> {
AUTH_PLANE_DEPS.get_or_init(|| RwLock::new(None))
}
pub fn install_auth_plane_deps(deps: Arc<AuthPlaneDeps>) {
if let Ok(mut guard) = auth_plane_deps_cell().write() {
*guard = Some(deps);
}
}
fn auth_plane_deps() -> Option<Arc<AuthPlaneDeps>> {
auth_plane_deps_cell()
.read()
.ok()
.and_then(|guard| guard.as_ref().map(Arc::clone))
}
pub(crate) async fn auth_plane_pg_reachable() -> Option<bool> {
let deps = auth_plane_deps()?;
Some(pg_pool_reachable(&deps.pool).await)
}
pub(crate) async fn pg_pool_reachable(pool: &sqlx::PgPool) -> bool {
matches!(
tokio::time::timeout(
std::time::Duration::from_secs(2),
sqlx::query("SELECT 1").execute(pool),
)
.await,
Ok(Ok(_))
)
}
fn now_unix() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
fn header_value(headers: &http::HeaderMap, name: &str) -> String {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.trim()
.to_string()
}
fn first_peer_cert_der(extensions: &http::Extensions) -> Option<Vec<u8>> {
use tonic::transport::server::{TcpConnectInfo, TlsConnectInfo};
extensions
.get::<TlsConnectInfo<TcpConnectInfo>>()
.and_then(|info| info.peer_certs())
.as_deref()
.and_then(|certs| certs.first())
.map(|cert| cert.as_ref().to_vec())
}
async fn resolve_credentials(
headers: &http::HeaderMap,
peer_cert_der: Option<Vec<u8>>,
) -> Option<PreresolvedCredentials> {
let bearer = headers
.get("authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.trim().strip_prefix("Bearer "))
.map(str::to_string);
let api_key = {
let key = {
let primary = header_value(headers, "x-api-key");
if primary.is_empty() {
header_value(headers, "x-udb-api-key")
} else {
primary
}
};
if key.is_empty() {
None
} else {
auth_plane_deps().map(|deps| {
(deps, key)
})
}
};
let has_cert = peer_cert_der.is_some();
if bearer.is_none() && api_key.is_none() && !has_cert {
return None;
}
let mut resolved = PreresolvedCredentials::default();
if let Some(token) = bearer {
let outcome = match crate::runtime::security::validate_bearer_token_cached(
&crate::runtime::security::SecurityConfig::current(),
&token,
) {
Ok(claims) => {
let principal = VerifiedPrincipal::from_verified_bearer_claims(&claims);
if principal.service_identity.trim().is_empty() {
Ok(principal)
} else if let Some(deps) = auth_plane_deps() {
match crate::runtime::service::auth_service::grants::validate_service_principal_against_grant(
&deps.pool,
&principal.tenant_id,
&principal.subject,
&principal.project_id,
&principal.service_identity,
&principal.scopes,
)
.await
{
Ok(true) => Ok(principal),
Ok(false) => Err("service bearer is not backed by a current typed grant".to_string()),
Err(error) => Err(error),
}
} else {
Err(
"service bearer validation requires the durable typed-grant store"
.to_string(),
)
}
}
Err(error) => Err(error),
};
resolved.bearer = Some(outcome);
}
if let Some((deps, key)) = api_key {
let outcome = match crate::runtime::authn::validate_api_key(
deps.api_key_store.as_ref(),
&key,
&deps.api_key_hash_key,
now_unix(),
)
.await
{
Ok(Some(record)) => {
match crate::runtime::service::auth_service::grants::attenuate_key_scopes_against_grant(
&deps.pool,
&record.tenant_id,
&record.principal_id,
record.grant_revision,
&record.scopes,
)
.await
{
Ok(Some(effective_scopes)) => Ok(Some(VerifiedPrincipal {
credential_type: 3, subject: record.principal_id,
service_identity: record.service_identity,
tenant_id: record.tenant_id,
project_id: record.project_id,
scopes: effective_scopes,
roles: Vec::new(),
credential_id: record.key_prefix,
auth_method: "api_key".to_string(),
certificate_identity: None,
})),
Ok(None) => Ok(None),
Err(error) => Err(error),
}
}
Ok(None) => Ok(None),
Err(error) => Err(error),
};
resolved.api_key = Some(outcome);
}
if let Some(der) = peer_cert_der {
resolved.certificate_present = true;
resolved.certificate_identity = crate::runtime::security::service_identity_from_der(&der);
{
if let Some(deps) = auth_plane_deps() {
match crate::runtime::service::auth_service::grants::resolve_certificate_grant(
&deps.pool, &der,
)
.await
{
Ok(Some(grant)) => {
resolved.certificate_principal = Some(VerifiedPrincipal {
credential_type: 5, subject: grant.subject,
service_identity: grant.service_identity,
tenant_id: grant.tenant_id,
project_id: grant.project_id,
scopes: grant.scopes,
roles: Vec::new(),
credential_id: grant.credential_id,
auth_method: "mtls".to_string(),
certificate_identity: resolved.certificate_identity.clone(),
});
}
Ok(None) => {
}
Err(error) => {
if crate::runtime::executor_utils::status_from_store_string(error.clone())
.code()
== tonic::Code::Unavailable
{
resolved.certificate_resolution_unavailable = true;
} else {
resolved.certificate_resolution_failed = true;
}
tracing::warn!(error = %error,
"certificate binding resolution failed (fail closed)");
}
}
} else {
resolved.certificate_resolution_failed = true;
}
}
}
Some(resolved)
}
#[derive(Clone, Default)]
pub struct CredentialResolveLayer;
impl CredentialResolveLayer {
pub fn new() -> Self {
Self
}
}
impl<S> tower::Layer<S> for CredentialResolveLayer {
type Service = CredentialResolveService<S>;
fn layer(&self, inner: S) -> Self::Service {
CredentialResolveService { inner }
}
}
#[derive(Clone)]
pub struct CredentialResolveService<S> {
inner: S,
}
impl<S> tonic::server::NamedService for CredentialResolveService<S>
where
S: tonic::server::NamedService,
{
const NAME: &'static str = S::NAME;
}
impl<S, ReqBody> Service<http::Request<ReqBody>> for CredentialResolveService<S>
where
S: Service<http::Request<ReqBody>, Response = http::Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ReqBody: Send + 'static,
{
type Response = http::Response<BoxBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<http::Response<BoxBody>, S::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: http::Request<ReqBody>) -> Self::Future {
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move {
let needs_resolution = req.headers().contains_key("x-api-key")
|| req.headers().contains_key("x-udb-api-key")
|| req
.headers()
.get("authorization")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.trim().starts_with("Bearer "))
|| req
.extensions()
.get::<tonic::transport::server::TlsConnectInfo<
tonic::transport::server::TcpConnectInfo,
>>()
.is_some();
if needs_resolution {
let der = first_peer_cert_der(req.extensions());
if let Some(resolved) = resolve_credentials(req.headers(), der).await {
req.extensions_mut().insert(resolved);
}
}
inner.call(req).await
})
}
}