Skip to content

Commit b9092de

Browse files
committed
Narrow gateway runtime surface
Keep registry-backed policy details behind the policy construction boundary and trim unused dependency features so the gateway exposes less non-core surface.
1 parent c02bf01 commit b9092de

9 files changed

Lines changed: 290 additions & 308 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ version = "0.1.0"
77
edition = "2024"
88

99
[dependencies]
10-
tokio = { version = "1", features = ["full"] }
11-
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "logging", "std", "tls12"] }
12-
tokio-rustls = { version = "0.26", default-features = false, features = ["aws-lc-rs", "logging", "tls12"] }
10+
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "time"] }
11+
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std", "tls12"] }
12+
tokio-rustls = { version = "0.26", default-features = false, features = ["aws-lc-rs", "tls12"] }
1313
rustls-pemfile = "2"
1414
rustls-pki-types = "1"
1515
x509-parser = { version = "0.18", default-features = false }
1616
hyper = { version = "1", features = ["server", "http2"] }
17-
hyper-util = { version = "0.1", features = ["tokio", "server-auto", "server-graceful"] }
17+
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
1818
http = "1"
1919
http-body-util = "0.1"
2020
bytes = "1"
@@ -26,14 +26,15 @@ tracing = "0.1"
2626
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
2727
tracing-opentelemetry = "0.32"
2828
opentelemetry = "0.31"
29-
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio"] }
30-
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic"] }
29+
opentelemetry_sdk = { version = "0.31", default-features = false, features = ["rt-tokio", "trace"] }
30+
opentelemetry-otlp = { version = "0.31", default-features = false, features = ["grpc-tonic", "trace"] }
3131
anyhow = "1"
32-
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres", "chrono", "migrate", "macros", "derive"] }
33-
chrono = { version = "0.4.44", features = ["serde"] }
32+
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres", "chrono", "derive"] }
33+
chrono = "0.4.44"
3434
p256 = { version = "0.13.2", features = ["ecdsa", "pkcs8"] }
3535

3636
[dev-dependencies]
3737
rcgen = { version = "0.14", features = ["x509-parser"] }
3838
tokio-test = "0.4"
3939
hyper = { version = "1", features = ["client"] }
40+
sqlx = { version = "0.8.6", default-features = false, features = ["migrate", "macros"] }

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ pub mod config;
22
pub mod observability;
33
pub mod policy;
44
pub mod proxy;
5-
pub mod registry;
5+
mod registry;
66
pub mod tls;

src/main.rs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use std::path::PathBuf;
2-
use std::str::FromStr;
32
use std::sync::Arc;
43

5-
use agent_gateway::policy::PostgresPolicyEngine;
64
use agent_gateway::proxy::MakeProxyService;
7-
use agent_gateway::{config, observability, policy, proxy, registry, tls};
5+
use agent_gateway::{config, observability, policy, proxy, tls};
86
use anyhow::Context;
97
use clap::Parser;
108
use hyper_util::rt::TokioExecutor;
11-
use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions};
129
use tokio::net::TcpListener;
1310
use tracing::{error, info};
1411

@@ -39,14 +36,7 @@ async fn serve(config: config::Config) -> anyhow::Result<()> {
3936
let server_tls = tls::build_server_config(&config.server)?;
4037
let tls_acceptor = tls::TlsAcceptor::from(server_tls);
4138

42-
let db_pool = build_pg_pool(&config.policy).await?;
43-
registry::RegistryStore::verify_schema_version(&db_pool).await?;
44-
let registry = registry::RegistryStore::new(db_pool, config.policy.query_timeout());
45-
let policy_engine: Arc<dyn policy::PolicyEngine> = Arc::new(PostgresPolicyEngine::new(
46-
&config.policy.client_ext_oid,
47-
registry,
48-
)?);
49-
39+
let policy_engine = policy::build_engine(&config.policy).await?;
5040
let make_service = Arc::new(MakeProxyService::new(policy_engine));
5141

5242
let listen_addr: std::net::SocketAddr = config.server.listen_addr.parse()?;
@@ -66,22 +56,6 @@ async fn serve(config: config::Config) -> anyhow::Result<()> {
6656
Ok(())
6757
}
6858

69-
async fn build_pg_pool(policy: &config::PolicyConfig) -> anyhow::Result<PgPool> {
70-
let database_url = policy.database_url()?;
71-
let connect_options = PgConnectOptions::from_str(&database_url)
72-
.context("parsing authorization registry database URL")?;
73-
74-
let connect = PgPoolOptions::new()
75-
.max_connections(policy.max_connections())
76-
.acquire_timeout(policy.pool_acquire_timeout())
77-
.connect_with(connect_options);
78-
79-
tokio::time::timeout(policy.connect_timeout(), connect)
80-
.await
81-
.context("authorization registry database connect timed out")?
82-
.context("connecting to authorization registry database")
83-
}
84-
8559
async fn serve_loop(
8660
listener: &TcpListener,
8761
tls_acceptor: &tls::TlsAcceptor,

src/policy.rs

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
use std::str::FromStr;
2+
use std::sync::Arc;
23

4+
use anyhow::Context;
35
use async_trait::async_trait;
46
use chrono::SecondsFormat;
57
use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
68
use p256::pkcs8::DecodePublicKey;
79
use rustls_pki_types::CertificateDer;
10+
use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions};
811
use x509_parser::oid_registry::Oid;
912
use x509_parser::prelude::*;
1013

14+
use crate::config::PolicyConfig;
1115
use crate::registry::{CandidatePermission, RegistryStore};
1216

1317
pub struct RequestContext {
@@ -30,13 +34,13 @@ pub trait PolicyEngine: Send + Sync + 'static {
3034
async fn evaluate(&self, ctx: &RequestContext) -> PolicyDecision;
3135
}
3236

33-
pub struct PostgresPolicyEngine {
37+
struct PostgresPolicyEngine {
3438
client_ext_oid: Oid<'static>,
3539
registry: RegistryStore,
3640
}
3741

3842
impl PostgresPolicyEngine {
39-
pub fn new(client_ext_oid: &str, registry: RegistryStore) -> anyhow::Result<Self> {
43+
fn new(client_ext_oid: &str, registry: RegistryStore) -> anyhow::Result<Self> {
4044
let client_ext_oid = parse_client_ext_oid(client_ext_oid)?;
4145

4246
Ok(Self {
@@ -46,7 +50,33 @@ impl PostgresPolicyEngine {
4650
}
4751
}
4852

49-
pub fn parse_client_ext_oid(value: &str) -> anyhow::Result<Oid<'static>> {
53+
pub async fn build_engine(config: &PolicyConfig) -> anyhow::Result<Arc<dyn PolicyEngine>> {
54+
let db_pool = build_pg_pool(config).await?;
55+
RegistryStore::verify_schema_version(&db_pool).await?;
56+
let registry = RegistryStore::new(db_pool, config.query_timeout());
57+
Ok(Arc::new(PostgresPolicyEngine::new(
58+
&config.client_ext_oid,
59+
registry,
60+
)?))
61+
}
62+
63+
async fn build_pg_pool(policy: &PolicyConfig) -> anyhow::Result<PgPool> {
64+
let database_url = policy.database_url()?;
65+
let connect_options = PgConnectOptions::from_str(&database_url)
66+
.context("parsing authorization registry database URL")?;
67+
68+
let connect = PgPoolOptions::new()
69+
.max_connections(policy.max_connections())
70+
.acquire_timeout(policy.pool_acquire_timeout())
71+
.connect_with(connect_options);
72+
73+
tokio::time::timeout(policy.connect_timeout(), connect)
74+
.await
75+
.context("authorization registry database connect timed out")?
76+
.context("connecting to authorization registry database")
77+
}
78+
79+
pub(crate) fn parse_client_ext_oid(value: &str) -> anyhow::Result<Oid<'static>> {
5080
Oid::from_str(value).map_err(|e| anyhow::anyhow!("invalid policy.client_ext_oid: {e:?}"))
5181
}
5282

@@ -59,7 +89,7 @@ pub fn parse_client_ext_oid(value: &str) -> anyhow::Result<Oid<'static>> {
5989
/// - `[ipv6]:port` → `[ipv6]:port`
6090
/// - `[ipv6]` → `[ipv6]:443`
6191
/// - bare `ipv6` (colons, no brackets) → `[ipv6]:443`
62-
pub fn normalize_destination(dest: &str) -> anyhow::Result<String> {
92+
pub(crate) fn normalize_destination(dest: &str) -> anyhow::Result<String> {
6393
let dest = dest.trim();
6494
anyhow::ensure!(!dest.is_empty(), "destination must not be empty");
6595

@@ -308,6 +338,113 @@ mod tests {
308338
use super::*;
309339
use chrono::{TimeZone, Utc};
310340

341+
#[test]
342+
fn normalize_plain_hostname_defaults_to_443() {
343+
assert_eq!(
344+
normalize_destination("api.example.com").unwrap(),
345+
"api.example.com:443"
346+
);
347+
}
348+
349+
#[test]
350+
fn normalize_hostname_with_explicit_port() {
351+
assert_eq!(
352+
normalize_destination("api.example.com:8080").unwrap(),
353+
"api.example.com:8080"
354+
);
355+
}
356+
357+
#[test]
358+
fn normalize_hostname_with_443() {
359+
assert_eq!(
360+
normalize_destination("api.example.com:443").unwrap(),
361+
"api.example.com:443"
362+
);
363+
}
364+
365+
#[test]
366+
fn normalize_lowercases_hostname() {
367+
assert_eq!(
368+
normalize_destination("API.EXAMPLE.COM:443").unwrap(),
369+
"api.example.com:443"
370+
);
371+
assert_eq!(
372+
normalize_destination("API.EXAMPLE.COM").unwrap(),
373+
"api.example.com:443"
374+
);
375+
}
376+
377+
#[test]
378+
fn normalize_bracketed_ipv6_with_port() {
379+
assert_eq!(normalize_destination("[::1]:8443").unwrap(), "[::1]:8443");
380+
}
381+
382+
#[test]
383+
fn normalize_bracketed_ipv6_without_port_defaults_to_443() {
384+
assert_eq!(normalize_destination("[::1]").unwrap(), "[::1]:443");
385+
}
386+
387+
#[test]
388+
fn normalize_bare_ipv6_defaults_to_443() {
389+
assert_eq!(normalize_destination("::1").unwrap(), "[::1]:443");
390+
assert_eq!(
391+
normalize_destination("2001:db8::1").unwrap(),
392+
"[2001:db8::1]:443"
393+
);
394+
}
395+
396+
#[test]
397+
fn normalize_bare_ipv6_lowercases() {
398+
assert_eq!(normalize_destination("FE80::1").unwrap(), "[fe80::1]:443");
399+
}
400+
401+
#[test]
402+
fn normalize_rejects_empty() {
403+
assert!(normalize_destination("").is_err());
404+
assert!(normalize_destination(" ").is_err());
405+
}
406+
407+
#[test]
408+
fn normalize_rejects_empty_bracketed_host() {
409+
assert!(normalize_destination("[]").is_err());
410+
assert!(normalize_destination("[]:443").is_err());
411+
}
412+
413+
#[test]
414+
fn normalize_rejects_missing_close_bracket() {
415+
assert!(normalize_destination("[::1").is_err());
416+
}
417+
418+
#[test]
419+
fn normalize_rejects_non_numeric_port() {
420+
assert!(normalize_destination("host:abc").is_err());
421+
}
422+
423+
#[test]
424+
fn normalize_rejects_port_zero() {
425+
assert!(normalize_destination("host:0").is_err());
426+
assert!(normalize_destination("[::1]:0").is_err());
427+
}
428+
429+
#[test]
430+
fn normalize_rejects_invalid_multi_colon() {
431+
assert!(normalize_destination("foo:bar:baz").is_err());
432+
assert!(normalize_destination("api.example.com:443:extra").is_err());
433+
}
434+
435+
#[test]
436+
fn normalize_rejects_invalid_bracketed_host() {
437+
assert!(normalize_destination("[not-ipv6]:443").is_err());
438+
}
439+
440+
#[test]
441+
fn normalize_trims_whitespace() {
442+
assert_eq!(
443+
normalize_destination(" api.example.com:443 ").unwrap(),
444+
"api.example.com:443"
445+
);
446+
}
447+
311448
#[test]
312449
fn canonical_permission_bytes_are_stable() {
313450
let candidate = CandidatePermission {

0 commit comments

Comments
 (0)