diff --git a/crates/validator/src/alternator/auth.rs b/crates/validator/src/alternator/auth.rs index aafb070d3..c36637b03 100644 --- a/crates/validator/src/alternator/auth.rs +++ b/crates/validator/src/alternator/auth.rs @@ -18,7 +18,6 @@ use crate::common; use scylla::client::session::Session; use std::net::Ipv4Addr; use std::sync::Arc; -use std::sync::LazyLock; use tracing::info; use uuid::Uuid; @@ -27,27 +26,6 @@ use crate::alternator::ALTERNATOR_PORT; use crate::alternator::JsonBodyInjectInterceptor; use aws_sdk_dynamodb::error::ProvideErrorMetadata as _; -static SUPERUSER_NAME: LazyLock = LazyLock::new(|| Uuid::new_v4().simple().to_string()); -static SUPERUSER_PASSWORD: LazyLock = LazyLock::new(|| Uuid::new_v4().simple().to_string()); -static SUPERUSER_SALTED_PASSWORD: LazyLock = LazyLock::new(|| { - bcrypt::hash(&*SUPERUSER_PASSWORD, bcrypt::DEFAULT_COST) - .expect("failed to hash superuser password") -}); - -/// Builds the ScyllaDB extra-config YAML that enables password authentication, -/// CQL authorization, and sets the superuser credentials. -fn scylla_auth_config() -> Vec { - let name = &*SUPERUSER_NAME; - let salted = &*SUPERUSER_SALTED_PASSWORD; - format!( - "authenticator: PasswordAuthenticator\n\ - authorizer: CassandraAuthorizer\n\ - auth_superuser_name: '{name}'\n\ - auth_superuser_salted_password: '{salted}'" - ) - .into_bytes() -} - /// Polls the Alternator endpoint with the given credentials until it responds /// successfully. With `--alternator-enforce-authorization=true`, the standard /// `wait_for_alternator` (which uses dummy `"any"/"any"` creds) would loop @@ -97,8 +75,12 @@ async fn alternator_with_auth_enabled(actors: Arc) { let db_ip = actors.services_subnet.ip(common::DB_OCTET_1); info!("Connecting to ScyllaDB as superuser"); - let (session, vs_clients) = - common::prepare_connection_with_auth(&actors, &SUPERUSER_NAME, &SUPERUSER_PASSWORD).await; + let (session, vs_clients) = common::prepare_connection_with_auth( + &actors, + &common::SUPERUSER_NAME, + &common::SUPERUSER_PASSWORD, + ) + .await; let role_name = Uuid::new_v4().simple().to_string(); let role_password = Uuid::new_v4().simple().to_string(); @@ -127,9 +109,9 @@ async fn alternator_with_auth_enabled(actors: Arc) { info!("Fetching salted_hash for limited role '{role_name}'"); let limited_salted_hash = get_salted_hash(&session, &role_name).await; - let superuser_salted_hash = get_salted_hash(&session, &SUPERUSER_NAME).await; + let superuser_salted_hash = get_salted_hash(&session, &common::SUPERUSER_NAME).await; - wait_for_alternator_with_creds(db_ip, &SUPERUSER_NAME, &superuser_salted_hash).await; + wait_for_alternator_with_creds(db_ip, &common::SUPERUSER_NAME, &superuser_salted_hash).await; let limited_client = alternator::make_dynamodb_client_with_creds(db_ip, &role_name, &limited_salted_hash).await; @@ -283,17 +265,17 @@ impl e2etest::Fixture for Fixture { let scylla_configs = alternator::get_scylla_configs( &actors, [("--alternator-enforce-authorization", "true")], - Some(scylla_auth_config()), + Some(common::scylla_auth_config()), ) .await; let mut vs_configs = common::get_default_vs_node_configs(&actors).await; for config in &mut vs_configs { - config.user = Some(SUPERUSER_NAME.clone()); - config.password = Some(SUPERUSER_PASSWORD.clone()); + config.user = Some(common::SUPERUSER_NAME.clone()); + config.password = Some(common::SUPERUSER_PASSWORD.clone()); } - common::init_with_config(&actors, scylla_configs, vs_configs).await; + common::init_with_config(&actors, scylla_configs, vs_configs, false).await; Self { actors } } diff --git a/crates/validator/src/alternator/mod.rs b/crates/validator/src/alternator/mod.rs index 04bd200c9..1585191f8 100644 --- a/crates/validator/src/alternator/mod.rs +++ b/crates/validator/src/alternator/mod.rs @@ -654,7 +654,9 @@ async fn get_scylla_configs( config.args.retain(|arg| !arg.starts_with(name)); config.args.push(format!("{name}={value}")); } - config.extra_config = extra_config.clone(); + if let Some(extra_config) = extra_config.clone() { + config.extra_config = Some(extra_config); + } } scylla_configs } @@ -669,7 +671,7 @@ async fn init_with_args(actors: &TestActors, extra_args: impl IntoIterator = LazyLock::new(|| Uuid::new_v4().simple().to_string()); -static SUPERUSER_PASSWORD: LazyLock = LazyLock::new(|| Uuid::new_v4().simple().to_string()); -static SUPERUSER_SALTED_PASSWORD: LazyLock = LazyLock::new(|| { - bcrypt::hash(&*SUPERUSER_PASSWORD, bcrypt::DEFAULT_COST) - .expect("failed to hash superuser password") -}); - -/// Builds the ScyllaDB config YAML with authentication enabled and superuser credentials set. -fn scylla_auth_config() -> Vec { - let name = &*SUPERUSER_NAME; - let salted = &*SUPERUSER_SALTED_PASSWORD; - format!( - "authenticator: PasswordAuthenticator\n\ - authorizer: CassandraAuthorizer\n\ - auth_superuser_name: '{name}'\n\ - auth_superuser_salted_password: '{salted}'" - ) - .into_bytes() -} - e2etest::group!(name = auth, fixtures = (Fixture), parent = crate::validator); struct Fixture { diff --git a/crates/validator/src/common.rs b/crates/validator/src/common.rs index 8e6943ea1..a23261492 100644 --- a/crates/validator/src/common.rs +++ b/crates/validator/src/common.rs @@ -29,6 +29,7 @@ use std::collections::HashMap; use std::iter; use std::net::Ipv4Addr; use std::sync::Arc; +use std::sync::LazyLock; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::time::Duration; @@ -36,6 +37,7 @@ use std::time::Instant; use tap::Pipe; use tokio::time; use tracing::info; +use uuid::Uuid; pub const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10 * 60); // 10 minutes pub const DEFAULT_OPERATION_TIMEOUT: Duration = Duration::from_secs(20); @@ -55,6 +57,75 @@ pub const VS_OCTET_1: u8 = 21; pub const VS_OCTET_2: u8 = 22; pub const VS_OCTET_3: u8 = 23; +// Credentials for the default, reduced-privilege role that the Vector Store +// uses to connect to ScyllaDB, mirroring the setup used in production deployments. +pub const DEFAULT_DB_USER: &str = "vector_store"; +pub const DEFAULT_DB_PASSWORD: &str = "vector_store_password"; + +/// Fine-grained indexing permissions granted to `DEFAULT_DB_USER`, mirroring +/// the reduced-privilege role used in production deployments rather than full `SELECT` access. +const DEFAULT_ROLE_PERMISSIONS: &[&str] = &["VECTOR_SEARCH_INDEXING", "TEXT_SEARCH_INDEXING"]; + +pub(crate) static SUPERUSER_NAME: LazyLock = + LazyLock::new(|| Uuid::new_v4().simple().to_string()); +pub(crate) static SUPERUSER_PASSWORD: LazyLock = + LazyLock::new(|| Uuid::new_v4().simple().to_string()); +static SUPERUSER_SALTED_PASSWORD: LazyLock = LazyLock::new(|| { + bcrypt::hash(&*SUPERUSER_PASSWORD, bcrypt::DEFAULT_COST) + .expect("failed to hash superuser password") +}); + +/// Builds the ScyllaDB config YAML with authentication enabled and superuser credentials set. +pub fn scylla_auth_config() -> Vec { + let name = &*SUPERUSER_NAME; + let salted = &*SUPERUSER_SALTED_PASSWORD; + format!( + "authenticator: PasswordAuthenticator\n\ + authorizer: CassandraAuthorizer\n\ + auth_superuser_name: '{name}'\n\ + auth_superuser_salted_password: '{salted}'" + ) + .into_bytes() +} + +/// Connects to ScyllaDB as the superuser and creates `DEFAULT_DB_USER`, +/// granting it the reduced-privilege indexing permissions +/// (`DEFAULT_ROLE_PERMISSIONS`) that the Vector Store uses by default. +#[framed] +async fn setup_default_role(actors: &TestActors, tls: bool) { + info!("Setting up default role '{DEFAULT_DB_USER}' with indexing permissions"); + + let session = if tls { + prepare_connection_with_auth(actors, &SUPERUSER_NAME, &SUPERUSER_PASSWORD) + .await + .0 + } else { + prepare_connection_with_auth_no_tls(actors, &SUPERUSER_NAME, &SUPERUSER_PASSWORD) + .await + .0 + }; + + session + .query_unpaged( + format!( + "CREATE ROLE {DEFAULT_DB_USER} WITH PASSWORD = '{DEFAULT_DB_PASSWORD}' AND LOGIN = true" + ), + (), + ) + .await + .expect("failed to create default role"); + + for permission in DEFAULT_ROLE_PERMISSIONS { + let query = format!("GRANT {permission} ON ALL KEYSPACES TO {DEFAULT_DB_USER}"); + session + .query_unpaged(query, ()) + .await + .unwrap_or_else(|err| { + panic!("failed to grant {permission} to {DEFAULT_DB_USER}: {err}") + }); + } +} + #[derive( Clone, Debug, PartialEq, Eq, Hash, derive_more::From, derive_more::AsRef, derive_more::Display, )] @@ -120,7 +191,7 @@ pub async fn get_default_scylla_node_configs(actors: &TestActors) -> Vec Vec Vec, vs_configs: Vec, + use_default_auth: bool, ) { init_dns(actors).await; actors.db.start(scylla_configs).await; assert!(actors.db.wait_for_ready().await); actors.vs.start(vs_configs).await; + + if use_default_auth { + setup_default_role(actors, true).await; + } + assert!(actors.vs.wait_for_ready().await); } @@ -350,6 +433,7 @@ pub async fn prepare_connection_with_custom_vs_ips( let session = Arc::new( SessionBuilder::new() .known_node(actors.services_subnet.ip(DB_OCTET_1).to_string()) + .user(&*SUPERUSER_NAME, &*SUPERUSER_PASSWORD) .tls_context(Some(TlsContext::from(tls_config))) .build() .await @@ -386,6 +470,28 @@ pub async fn prepare_connection_with_auth( (session, clients) } +#[framed] +pub async fn prepare_connection_with_auth_no_tls( + actors: &TestActors, + user: &str, + password: &str, +) -> (Arc, Vec) { + let session = Arc::new( + SessionBuilder::new() + .known_node(actors.services_subnet.ip(DB_OCTET_1).to_string()) + .user(user, password) + .build() + .await + .expect("failed to create session"), + ); + let vs_ips = get_default_vs_ips(actors); + let clients = vs_ips + .iter() + .map(|&ip| HttpClient::new((ip, VS_PORT).into())) + .collect(); + (session, clients) +} + #[framed] pub async fn prepare_connection(actors: &TestActors) -> (Arc, Vec) { prepare_connection_with_custom_vs_ips(actors, get_default_vs_ips(actors)).await @@ -428,6 +534,7 @@ pub async fn prepare_connection_with_custom_vs_ips_no_tls( let session = Arc::new( SessionBuilder::new() .known_node(actors.services_subnet.ip(DB_OCTET_1).to_string()) + .user(&*SUPERUSER_NAME, &*SUPERUSER_PASSWORD) .build() .await .expect("failed to create session"), diff --git a/crates/validator/src/high_availability.rs b/crates/validator/src/high_availability.rs index 89f0f7e50..6f0dd99cf 100644 --- a/crates/validator/src/high_availability.rs +++ b/crates/validator/src/high_availability.rs @@ -54,7 +54,7 @@ async fn test_secondary_uri_works_correctly(actors: Arc) { args: e2etest_scylla_cluster::default_scylla_args(), cert_path: Some(cert_path.clone()), key_path: Some(key_path.clone()), - extra_config: None, + extra_config: Some(scylla_auth_config()), }, ScyllaNodeConfig { db_ip: actors.services_subnet.ip(DB_OCTET_2), @@ -63,7 +63,7 @@ async fn test_secondary_uri_works_correctly(actors: Arc) { args: e2etest_scylla_cluster::default_scylla_args(), cert_path: Some(cert_path.clone()), key_path: Some(key_path.clone()), - extra_config: None, + extra_config: Some(scylla_auth_config()), }, ScyllaNodeConfig { db_ip: actors.services_subnet.ip(DB_OCTET_3), @@ -72,7 +72,7 @@ async fn test_secondary_uri_works_correctly(actors: Arc) { args: e2etest_scylla_cluster::default_scylla_args(), cert_path: Some(cert_path.clone()), key_path: Some(key_path.clone()), - extra_config: None, + extra_config: Some(scylla_auth_config()), }, ]; let cert_env = actors @@ -89,10 +89,10 @@ async fn test_secondary_uri_works_correctly(actors: Arc) { "VECTOR_STORE_SCYLLADB_CERTIFICATE_FILE".to_string(), cert_env, )]), - user: None, - password: None, + user: Some(DEFAULT_DB_USER.to_string()), + password: Some(DEFAULT_DB_PASSWORD.to_string()), }]; - init_with_config(&actors, scylla_configs, vs_configs).await; + init_with_config(&actors, scylla_configs, vs_configs, true).await; let vs_ips = vec![actors.services_subnet.ip(VS_OCTET_1)]; let (session, clients) = prepare_connection_with_custom_vs_ips(&actors, vs_ips).await; diff --git a/crates/validator/src/tls_reload.rs b/crates/validator/src/tls_reload.rs index 43a8434fe..b24cdcc2e 100644 --- a/crates/validator/src/tls_reload.rs +++ b/crates/validator/src/tls_reload.rs @@ -108,7 +108,7 @@ async fn reloads_tls_identity_after_cert_file_rotation(actors: Arc) ); } - init_with_config(&actors, scylla_configs, vs_configs).await; + init_with_config(&actors, scylla_configs, vs_configs, true).await; wait_for( || async { https_status_ok(&vs_ips, &cert_v1).await },