Some checks failed
CI/CD Pipeline / lint (push) Successful in 3m45s
CI/CD Pipeline / integration-tests (push) Failing after 57s
CI/CD Pipeline / unit-tests (push) Failing after 1m1s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped
- Fix proxy body forwarding, round-robin load balancing, response streaming - Pool reqwest::Client in proxy, control, and gateway (no per-request alloc) - Harden CORS in gateway/main.rs (was allow_origin(Any), now uses ALLOWED_ORIGINS) - Add common/src/error.rs: ApiError type with structured JSON responses - Add common/src/rls.rs: RlsTransaction extractor for deduplicated RLS setup - Fix tracing in all standalone binaries (EnvFilter instead of unused var) - Dockerfile multi-stage: separate worker-runtime, control-runtime, proxy-runtime targets - docker-compose.yml: split into worker/system/proxy services with health checks - Fix Grafana port mapping in pillar-system (3030:3000) - Add config/prometheus.yml and config/vmagent.yml - Add .env.example with all required variables - 55 tests pass (49 run + 6 ignored integration tests requiring external services) Made-with: Cursor
103 lines
3.3 KiB
Rust
103 lines
3.3 KiB
Rust
use crate::error::ApiError;
|
|
use sqlx::{PgPool, Postgres, Transaction};
|
|
|
|
const ALLOWED_ROLES: &[&str] = &["anon", "authenticated", "service_role"];
|
|
|
|
pub struct RlsTransaction {
|
|
pub tx: Transaction<'static, Postgres>,
|
|
}
|
|
|
|
impl RlsTransaction {
|
|
/// Begin a transaction with RLS context set.
|
|
/// `role` must be one of: anon, authenticated, service_role.
|
|
/// `sub` is the JWT subject claim (user ID), used for RLS policies.
|
|
pub async fn begin(
|
|
pool: &PgPool,
|
|
role: &str,
|
|
sub: Option<&str>,
|
|
) -> Result<Self, ApiError> {
|
|
let mut tx = pool.begin().await?;
|
|
|
|
// Validate and set role
|
|
if !ALLOWED_ROLES.contains(&role) {
|
|
return Err(ApiError::Forbidden("Invalid role".into()));
|
|
}
|
|
let role_query = format!("SET LOCAL role = '{}'", role);
|
|
sqlx::query(&role_query).execute(&mut *tx).await?;
|
|
|
|
// Set JWT claims for RLS policies
|
|
if let Some(sub) = sub {
|
|
sqlx::query("SELECT set_config('request.jwt.claim.sub', $1, true)")
|
|
.bind(sub)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
|
|
Ok(Self { tx })
|
|
}
|
|
|
|
pub async fn commit(self) -> Result<(), ApiError> {
|
|
self.tx.commit().await.map_err(ApiError::from)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for RlsTransaction {
|
|
type Target = Transaction<'static, Postgres>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.tx
|
|
}
|
|
}
|
|
|
|
impl std::ops::DerefMut for RlsTransaction {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.tx
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_rls_transaction_rejects_bad_role() {
|
|
// Verify role validation without needing a DB connection
|
|
assert!(ALLOWED_ROLES.contains(&"anon"));
|
|
assert!(ALLOWED_ROLES.contains(&"authenticated"));
|
|
assert!(ALLOWED_ROLES.contains(&"service_role"));
|
|
assert!(!ALLOWED_ROLES.contains(&"admin"));
|
|
assert!(!ALLOWED_ROLES.contains(&"superuser"));
|
|
assert!(!ALLOWED_ROLES.contains(&"'; DROP TABLE users; --"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires running PostgreSQL — run with: cargo test -- --ignored
|
|
async fn test_rls_transaction_sets_role() {
|
|
let pool = PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres")
|
|
.await
|
|
.expect("DB connection required");
|
|
|
|
let mut rls = RlsTransaction::begin(&pool, "authenticated", Some("user-123")).await.unwrap();
|
|
let row: (String,) = sqlx::query_as("SELECT current_setting('role')")
|
|
.fetch_one(&mut *rls.tx)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(row.0, "authenticated");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires running PostgreSQL — run with: cargo test -- --ignored
|
|
async fn test_rls_transaction_sets_claims() {
|
|
let pool = PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres")
|
|
.await
|
|
.expect("DB connection required");
|
|
|
|
let mut rls = RlsTransaction::begin(&pool, "authenticated", Some("user-abc-123")).await.unwrap();
|
|
let row: (String,) = sqlx::query_as("SELECT current_setting('request.jwt.claim.sub')")
|
|
.fetch_one(&mut *rls.tx)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(row.0, "user-abc-123");
|
|
}
|
|
}
|