Files
madbase/common/src/rls.rs
Vlad Durnea 0179cc285d
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
M1 foundation: fix proxy, pool HTTP clients, split services, add ApiError + RLS
- 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
2026-03-15 13:38:49 +02:00

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");
}
}