Skip to content

Commit bc81dc5

Browse files
committed
Enforce pedantic clippy lints
1 parent 20d3f87 commit bc81dc5

11 files changed

Lines changed: 185 additions & 118 deletions

File tree

.github/workflows/gateway.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
- uses: actions/checkout@v5
4242
- uses: dtolnay/rust-toolchain@stable
4343
- uses: Swatinem/rust-cache@v2
44+
- run: cargo clippy --locked --all-targets
4445
- run: cargo test --locked
4546

4647
image:

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name = "agent_gateway"
33
version = "0.1.0"
44
edition = "2024"
55

6+
[lints.clippy]
7+
pedantic = "deny"
8+
69
[dependencies]
710
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "time"] }
811
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std", "tls12"] }

src/config.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ pub struct PolicyConfig {
4141
}
4242

4343
impl Config {
44+
/// Load, parse, and validate a TOML config file.
45+
///
46+
/// # Errors
47+
///
48+
/// Returns an error if the file cannot be read, the TOML cannot be parsed,
49+
/// or any config value fails validation.
4450
pub fn load(path: &Path) -> anyhow::Result<Self> {
4551
let contents = std::fs::read_to_string(path)?;
4652
let config: Config = toml::from_str(&contents)?;
@@ -61,6 +67,12 @@ impl Config {
6167
}
6268

6369
impl PolicyConfig {
70+
/// Resolve the configured database URL from the literal value or env var.
71+
///
72+
/// # Errors
73+
///
74+
/// Returns an error if no source is configured, both sources are configured,
75+
/// the configured source is empty, or the environment variable cannot be read.
6476
pub fn database_url(&self) -> anyhow::Result<String> {
6577
match (&self.database_url, &self.database_url_env) {
6678
(Some(url), None) => {
@@ -89,18 +101,22 @@ impl PolicyConfig {
89101
}
90102
}
91103

104+
#[must_use]
92105
pub fn max_connections(&self) -> u32 {
93106
self.max_connections.unwrap_or(5)
94107
}
95108

109+
#[must_use]
96110
pub fn connect_timeout(&self) -> Duration {
97111
Duration::from_millis(self.connect_timeout_ms.unwrap_or(5_000))
98112
}
99113

114+
#[must_use]
100115
pub fn pool_acquire_timeout(&self) -> Duration {
101116
Duration::from_millis(self.pool_acquire_timeout_ms.unwrap_or(1_000))
102117
}
103118

119+
#[must_use]
104120
pub fn query_timeout(&self) -> Duration {
105121
Duration::from_millis(self.query_timeout_ms.unwrap_or(500))
106122
}
@@ -119,13 +135,13 @@ impl PolicyConfig {
119135

120136
match (&self.database_url, &self.database_url_env) {
121137
(Some(url), None) => {
122-
anyhow::ensure!(!url.is_empty(), "policy.database_url must not be empty")
138+
anyhow::ensure!(!url.is_empty(), "policy.database_url must not be empty");
123139
}
124140
(None, Some(env_name)) => {
125141
anyhow::ensure!(
126142
!env_name.is_empty(),
127143
"policy.database_url_env must not be empty"
128-
)
144+
);
129145
}
130146
(None, None) => {
131147
anyhow::bail!("policy.database_url or policy.database_url_env is required")

src/observability.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ use crate::config::ObservabilityConfig;
1212

1313
static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
1414

15+
/// Initialize tracing and optional OpenTelemetry export.
16+
///
17+
/// # Errors
18+
///
19+
/// Returns an error if the OTLP exporter cannot be built or the global tracing
20+
/// subscriber cannot be initialized.
1521
pub fn init(config: &ObservabilityConfig) -> anyhow::Result<()> {
1622
global::set_text_map_propagator(TraceContextPropagator::new());
1723

@@ -55,14 +61,12 @@ pub fn init(config: &ObservabilityConfig) -> anyhow::Result<()> {
5561
}
5662

5763
fn stdout_logging_enabled() -> bool {
58-
std::env::var("AGENT_GATEWAY_LOG_STDOUT")
59-
.map(|value| {
60-
!matches!(
61-
value.trim().to_ascii_lowercase().as_str(),
62-
"0" | "false" | "off" | "no"
63-
)
64-
})
65-
.unwrap_or(true)
64+
std::env::var("AGENT_GATEWAY_LOG_STDOUT").map_or(true, |value| {
65+
!matches!(
66+
value.trim().to_ascii_lowercase().as_str(),
67+
"0" | "false" | "off" | "no"
68+
)
69+
})
6670
}
6771

6872
pub fn shutdown() {

src/policy.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ impl PostgresPolicyEngine {
5050
}
5151
}
5252

53+
/// Build the configured policy engine.
54+
///
55+
/// # Errors
56+
///
57+
/// Returns an error if the database URL is invalid, the database cannot be
58+
/// reached, the schema version does not match, or the client extension OID is
59+
/// invalid.
5360
pub async fn build_engine(config: &PolicyConfig) -> anyhow::Result<Arc<dyn PolicyEngine>> {
5461
let db_pool = build_pg_pool(config).await?;
5562
RegistryStore::verify_schema_version(&db_pool).await?;
@@ -284,7 +291,7 @@ fn certificate_subject(
284291
return Err("extension value contains trailing bytes".into());
285292
}
286293
Ok(CertificateSubject {
287-
identity: v.string().to_owned(),
294+
identity: v.string().clone(),
288295
public_key_spki_der,
289296
})
290297
}

src/proxy.rs

Lines changed: 95 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl Extractor for HeaderExtractor<'_> {
3232
}
3333

3434
fn keys(&self) -> Vec<&str> {
35-
self.0.keys().map(|name| name.as_str()).collect()
35+
self.0.keys().map(http::HeaderName::as_str).collect()
3636
}
3737
}
3838

@@ -96,37 +96,22 @@ impl ProxyService {
9696
source_identity
9797
}
9898
PolicyDecision::Deny {
99-
source_identity: Some(source_identity),
99+
source_identity,
100100
reason,
101101
} => {
102-
warn!(
103-
source_identity = %source_identity,
104-
source_peer_addr = %self.source_peer_addr,
105-
dest_authority = %dest.authority,
106-
policy_decision = "deny",
107-
deny_reason = %reason,
108-
"CONNECT denied"
109-
);
110-
return response(StatusCode::FORBIDDEN, "forbidden");
111-
}
112-
PolicyDecision::Deny {
113-
source_identity: None,
114-
reason,
115-
} => {
116-
warn!(
117-
source_peer_addr = %self.source_peer_addr,
118-
dest_authority = %dest.authority,
119-
policy_decision = "deny",
120-
deny_reason = %reason,
121-
"CONNECT denied"
102+
log_denial(
103+
source_identity.as_deref(),
104+
self.source_peer_addr,
105+
&dest.authority,
106+
&reason,
122107
);
123108
return response(StatusCode::FORBIDDEN, "forbidden");
124109
}
125110
};
126111

127112
// Connect to destination BEFORE returning 200 so the client knows
128113
// the tunnel is actually established.
129-
let mut upstream = match TcpStream::connect((&*dest.host, dest.port)).await {
114+
let upstream = match TcpStream::connect((&*dest.host, dest.port)).await {
130115
Ok(s) => s,
131116
Err(e) => {
132117
error!(
@@ -141,50 +126,12 @@ impl ProxyService {
141126
};
142127

143128
let on_upgrade = hyper::upgrade::on(req);
144-
let source_peer_addr = self.source_peer_addr;
145-
146-
let tunnel_span = tracing::Span::current();
147-
tokio::spawn(
148-
async move {
149-
let upgraded = match on_upgrade.await {
150-
Ok(u) => u,
151-
Err(e) => {
152-
warn!(
153-
source_identity = %source_identity,
154-
source_peer_addr = %source_peer_addr,
155-
dest_authority = %dest.authority,
156-
error = %e,
157-
"upgrade failed"
158-
);
159-
return;
160-
}
161-
};
162-
163-
let mut downstream = hyper_util::rt::TokioIo::new(upgraded);
164-
165-
match copy_bidirectional(&mut downstream, &mut upstream).await {
166-
Ok((up, down)) => {
167-
info!(
168-
source_identity = %source_identity,
169-
source_peer_addr = %source_peer_addr,
170-
dest_authority = %dest.authority,
171-
bytes_client_to_dest = up,
172-
bytes_dest_to_client = down,
173-
"tunnel closed"
174-
);
175-
}
176-
Err(e) => {
177-
error!(
178-
source_identity = %source_identity,
179-
source_peer_addr = %source_peer_addr,
180-
dest_authority = %dest.authority,
181-
error = %e,
182-
"tunnel error"
183-
);
184-
}
185-
}
186-
}
187-
.instrument(tunnel_span),
129+
spawn_tunnel(
130+
on_upgrade,
131+
upstream,
132+
source_identity,
133+
self.source_peer_addr,
134+
dest.authority,
188135
);
189136

190137
response(StatusCode::OK, "")
@@ -214,6 +161,7 @@ impl MakeProxyService {
214161
Self { policy_engine }
215162
}
216163

164+
#[must_use]
217165
pub fn make_service(
218166
&self,
219167
peer_certs: Vec<CertificateDer<'static>>,
@@ -223,12 +171,91 @@ impl MakeProxyService {
223171
}
224172
}
225173

174+
#[must_use]
226175
pub fn extract_peer_certs(conn: &ServerConnection) -> Vec<CertificateDer<'static>> {
227176
conn.peer_certificates()
228-
.map(|certs| certs.to_vec())
177+
.map(<[CertificateDer<'_>]>::to_vec)
229178
.unwrap_or_default()
230179
}
231180

181+
fn log_denial(
182+
source_identity: Option<&str>,
183+
source_peer_addr: SocketAddr,
184+
dest_authority: &str,
185+
reason: &str,
186+
) {
187+
if let Some(source_identity) = source_identity {
188+
warn!(
189+
source_identity = %source_identity,
190+
source_peer_addr = %source_peer_addr,
191+
dest_authority = %dest_authority,
192+
policy_decision = "deny",
193+
deny_reason = %reason,
194+
"CONNECT denied"
195+
);
196+
} else {
197+
warn!(
198+
source_peer_addr = %source_peer_addr,
199+
dest_authority = %dest_authority,
200+
policy_decision = "deny",
201+
deny_reason = %reason,
202+
"CONNECT denied"
203+
);
204+
}
205+
}
206+
207+
fn spawn_tunnel(
208+
on_upgrade: hyper::upgrade::OnUpgrade,
209+
mut upstream: TcpStream,
210+
source_identity: String,
211+
source_peer_addr: SocketAddr,
212+
dest_authority: String,
213+
) {
214+
let tunnel_span = tracing::Span::current();
215+
tokio::spawn(
216+
async move {
217+
let upgraded = match on_upgrade.await {
218+
Ok(u) => u,
219+
Err(e) => {
220+
warn!(
221+
source_identity = %source_identity,
222+
source_peer_addr = %source_peer_addr,
223+
dest_authority = %dest_authority,
224+
error = %e,
225+
"upgrade failed"
226+
);
227+
return;
228+
}
229+
};
230+
231+
let mut downstream = hyper_util::rt::TokioIo::new(upgraded);
232+
233+
match copy_bidirectional(&mut downstream, &mut upstream).await {
234+
Ok((up, down)) => {
235+
info!(
236+
source_identity = %source_identity,
237+
source_peer_addr = %source_peer_addr,
238+
dest_authority = %dest_authority,
239+
bytes_client_to_dest = up,
240+
bytes_dest_to_client = down,
241+
"tunnel closed"
242+
);
243+
}
244+
Err(e) => {
245+
error!(
246+
source_identity = %source_identity,
247+
source_peer_addr = %source_peer_addr,
248+
dest_authority = %dest_authority,
249+
error = %e,
250+
"tunnel error"
251+
);
252+
}
253+
}
254+
}
255+
.instrument(tunnel_span),
256+
);
257+
}
258+
232259
fn response(status: StatusCode, message: &str) -> Response<ProxyBody> {
233260
let body: ProxyBody = if message.is_empty() {
234261
Empty::<Bytes>::new().boxed()

src/registry.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl RegistryStore {
6060
subject_public_key_spki_der: &[u8],
6161
) -> anyhow::Result<Vec<CandidatePermission>> {
6262
let query = sqlx::query_as::<_, CandidatePermission>(
63-
r#"
63+
r"
6464
SELECT
6565
p.permission_id,
6666
p.subject_identity,
@@ -90,7 +90,7 @@ impl RegistryStore {
9090
AND p.not_after > now()
9191
ORDER BY p.not_after DESC
9292
LIMIT 16
93-
"#,
93+
",
9494
)
9595
.bind(subject_identity)
9696
.bind(destination)
@@ -110,7 +110,7 @@ impl RegistryStore {
110110
permission_not_after: DateTime<Utc>,
111111
) -> anyhow::Result<bool> {
112112
let query = sqlx::query_scalar::<_, bool>(
113-
r#"
113+
r"
114114
SELECT EXISTS (
115115
SELECT 1
116116
FROM principal_key_permissions
@@ -122,7 +122,7 @@ impl RegistryStore {
122122
AND not_before <= $3
123123
AND not_after >= $4
124124
)
125-
"#,
125+
",
126126
)
127127
.bind(signing_key_id)
128128
.bind(destination)

0 commit comments

Comments
 (0)