udb 0.4.21

Universal Data Broker — a Rust gRPC broker over multiple databases (Postgres, MySQL, SQLite, MongoDB, ClickHouse, Cassandra, MSSQL, Redis, Qdrant, S3, Neo4j, …) with per-tenant RLS, 2PC, sagas, and CDC.
Documentation
use super::support::*;
use crate::proto::udb::core::authn::entity::v1 as authn_entity_pb;
use crate::proto::udb::core::authn::services::v1 as authn_pb;
use crate::proto::udb::core::authn::services::v1::authn_service_server::AuthnService;
use crate::runtime::authn::{
    AuthnConfig, PostgresApiKeyStore, PostgresUserStore, RedisSessionStore, SessionStore,
};
use crate::runtime::security::SecurityConfig;
use std::sync::Arc;
use tonic::Request;

fn live_redis_url() -> String {
    std::env::var("UDB_LIVE_AUTH_REDIS_URL")
        .or_else(|_| std::env::var("UDB_INTEGRATION_REDIS_URL"))
        .unwrap_or_else(|_| "redis://127.0.0.1:56379".to_string())
}

async fn flush_live_redis(url: &str) {
    let client = redis::Client::open(url).expect("create live redis client");
    let mut conn = client
        .get_multiplexed_async_connection()
        .await
        .expect("connect live redis");
    let _: String = redis::cmd("FLUSHDB")
        .query_async(&mut conn)
        .await
        .expect("flush live redis db");
}

#[tokio::test]
#[ignore = "requires live Postgres + Redis; run with UDB_LIVE_AUTH_TESTS=1 UDB_INTEGRATION_REDIS_URL=redis://127.0.0.1:56379 cargo test --lib live_redis_authn_session_store_roundtrip -- --ignored --nocapture"]
async fn live_redis_authn_session_store_roundtrip() {
    let _guard = live_auth_db_lock().lock().await;
    let pool = live_pg_pool().await;
    migrate_native_auth_db(&pool).await;
    let redis_url = live_redis_url();
    flush_live_redis(&redis_url).await;

    let config = AuthnConfig {
        session_enabled: true,
        session_hash_secret: "live-auth-test-secret".to_string(),
        session_backend: "redis".to_string(),
        session_ttl_secs: 120,
        ..AuthnConfig::default()
    };
    let sessions: Arc<dyn SessionStore> = Arc::new(
        RedisSessionStore::from_url(&redis_url, "udb:authn:live-test", 120)
            .expect("build Redis session store"),
    );
    let authn = super::super::AuthnServiceImpl::with_stores(
        config,
        SecurityConfig::current(),
        sessions,
        Arc::new(PostgresApiKeyStore::new(pool.clone(), "")),
        Arc::new(PostgresUserStore::new(pool.clone(), "")),
    );
    let user = create_verified_user(&authn, "redis_session", "CorrectHorse1!").await;

    let login = authn
        .login(Request::new(authn_pb::LoginRequest {
            username: user.email.clone(),
            password: "CorrectHorse1!".to_string(),
            device_name: "redis-live".to_string(),
            ..Default::default()
        }))
        .await
        .expect("login with Redis sessions")
        .into_inner();
    assert!(login.session_id.starts_with("sess_"));

    let valid = authn
        .validate_token(Request::new(authn_pb::ValidateTokenRequest {
            token: login.session_id.clone(),
            token_type: authn_entity_pb::TokenType::Session as i32,
        }))
        .await
        .expect("validate Redis-backed session")
        .into_inner();
    assert!(valid.valid);
    assert_eq!(valid.user_id, user.user_id);

    let listed = authn
        .list_sessions(Request::new(authn_pb::ListSessionsRequest {
            user_id: user.user_id.clone(),
            active_only: true,
            ..Default::default()
        }))
        .await
        .expect("list Redis-backed sessions")
        .into_inner();
    assert_eq!(listed.sessions.len(), 1);

    let sessions_model = crate::runtime::native_catalog::native_model(
        "udb.core.authn.entity.v1.Session",
        &["session_token_lookup"],
    );
    let postgres_session_count: i64 = sqlx::query_scalar(&format!(
        "SELECT COUNT(*)::bigint FROM {}",
        sessions_model.relation
    ))
    .fetch_one(&pool)
    .await
    .expect("count Postgres sessions");
    assert_eq!(
        postgres_session_count, 0,
        "Redis-backed authn must not persist sessions in Postgres"
    );

    authn
        .revoke_session(Request::new(authn_pb::RevokeSessionRequest {
            session_id: login.session_id.clone(),
            revoke_reason: "live_test".to_string(),
            ..Default::default()
        }))
        .await
        .expect("revoke Redis-backed session");
    let after_revoke = authn
        .validate_token(Request::new(authn_pb::ValidateTokenRequest {
            token: login.session_id,
            token_type: authn_entity_pb::TokenType::Session as i32,
        }))
        .await
        .expect("validate revoked Redis-backed session")
        .into_inner();
    assert!(!after_revoke.valid);

    flush_live_redis(&redis_url).await;
    cleanup_native_auth_db(&pool).await;
}