Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,82 @@ password = "production-admin-password-32-bytes"
);
}

#[test]
fn deploy_validation_rejects_example_publisher_hosts() {
let mut settings = valid_settings();
settings.publisher.domain = "example.com".to_string();
settings.publisher.cookie_domain = ".example.com".to_string();
settings.publisher.origin_url = "https://origin.example.com".to_string();

let err = validate_settings_for_deploy(&settings)
.expect_err("should reject unedited example publisher hosts");
let text = format!("{err:?}");

assert!(
text.contains("publisher.domain")
&& text.contains("publisher.cookie_domain")
&& text.contains("publisher.origin_url"),
"should flag all three example publisher placeholders: {err:?}"
);
}

#[test]
fn deploy_validation_rejects_placeholder_request_signing_store_ids() {
let mut settings = valid_settings();
settings.request_signing = Some(crate::settings::RequestSigning {
enabled: true,
config_store_id: "<management-config-store-id>".to_string(),
secret_store_id: "<management-secret-store-id>".to_string(),
});

let err = validate_settings_for_deploy(&settings)
.expect_err("should reject placeholder request-signing store ids when enabled");
let text = format!("{err:?}");

assert!(
text.contains("request_signing.config_store_id")
&& text.contains("request_signing.secret_store_id"),
"should flag both request-signing store ids: {err:?}"
);
}

#[test]
fn deploy_validation_allows_disabled_request_signing_with_placeholder_store_ids() {
let mut settings = valid_settings();
settings.request_signing = Some(crate::settings::RequestSigning {
enabled: false,
config_store_id: "<management-config-store-id>".to_string(),
secret_store_id: "<management-secret-store-id>".to_string(),
});

validate_settings_for_deploy(&settings)
.expect("should allow placeholder store ids while request signing is disabled");
}

#[test]
fn deploy_validation_rejects_placeholder_aps_pub_id() {
let mut settings = valid_settings();
settings
.integrations
.insert_config(
"aps",
&serde_json::json!({
"enabled": true,
"pub_id": "your-aps-publisher-id",
"endpoint": "https://aps.example.com/e/dtb/bid"
}),
)
.expect("should insert APS config");

let err = validate_settings_for_deploy(&settings)
.expect_err("should reject placeholder APS pub_id when enabled");

assert!(
format!("{err:?}").contains("aps"),
"should mention the APS integration: {err:?}"
);
}

#[test]
fn deploy_validation_rejects_external_prebid_bundle_without_proxy_allowed_domains() {
let mut settings = valid_settings();
Expand Down
23 changes: 22 additions & 1 deletion crates/trusted-server-core/src/integrations/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value as Json};
use std::collections::HashMap;
use std::time::Duration;
use validator::Validate;
use validator::{Validate, ValidationError};

use crate::auction::provider::AuctionProvider;
use crate::auction::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, MediaType};
Expand Down Expand Up @@ -201,6 +201,7 @@ pub struct ApsConfig {

/// APS publisher ID (accepts both string and integer from config)
#[serde(deserialize_with = "deserialize_pub_id")]
#[validate(length(min = 1), custom(function = validate_aps_pub_id))]
pub pub_id: String,

/// APS API endpoint
Expand Down Expand Up @@ -285,6 +286,26 @@ impl Default for ApsConfig {
}
}

/// Validator for [`ApsConfig::pub_id`]: rejects the known template placeholder
/// publisher ID. Non-emptiness is enforced by the built-in `length` validator;
/// this runs only when APS is enabled, because integration configs validate
/// lazily via `get_typed`.
fn validate_aps_pub_id(pub_id: &str) -> Result<(), ValidationError> {
if ApsConfig::PUB_ID_PLACEHOLDERS
.iter()
.any(|placeholder| placeholder.eq_ignore_ascii_case(pub_id.trim()))
{
return Err(ValidationError::new("aps_pub_id_placeholder"));
}
Ok(())
}

impl ApsConfig {
/// Reserved example `pub_id` values from the config template that must not
/// be deployed while APS is enabled.
pub const PUB_ID_PLACEHOLDERS: &[&str] = &["your-aps-publisher-id"];
}

impl IntegrationConfig for ApsConfig {
fn is_enabled(&self) -> bool {
self.enabled
Expand Down
70 changes: 70 additions & 0 deletions crates/trusted-server-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,39 @@
.any(|p| p.eq_ignore_ascii_case(proxy_secret))
}

/// Reserved example publisher values copied verbatim from the config
/// template. They deserialize fine but must be replaced before deploying.
const PLACEHOLDER_DOMAINS: &[&str] = &["example.com"];
const PLACEHOLDER_COOKIE_DOMAINS: &[&str] = &[".example.com"];
const PLACEHOLDER_ORIGIN_URLS: &[&str] = &["https://origin.example.com"];

/// Returns `true` if `domain` is the unedited template placeholder
/// (case-insensitive).
#[must_use]
pub fn is_placeholder_domain(domain: &str) -> bool {
Self::PLACEHOLDER_DOMAINS
.iter()
.any(|p| p.eq_ignore_ascii_case(domain.trim()))
}

/// Returns `true` if `cookie_domain` is the unedited template placeholder
/// (case-insensitive).
#[must_use]
pub fn is_placeholder_cookie_domain(cookie_domain: &str) -> bool {
Self::PLACEHOLDER_COOKIE_DOMAINS
.iter()
.any(|p| p.eq_ignore_ascii_case(cookie_domain.trim()))
}

/// Returns `true` if `origin_url` is the unedited template placeholder
/// (case-insensitive).
#[must_use]
pub fn is_placeholder_origin_url(origin_url: &str) -> bool {
Self::PLACEHOLDER_ORIGIN_URLS
.iter()
.any(|p| p.eq_ignore_ascii_case(origin_url.trim()))
}

/// Extracts the host (including port if present) from the `origin_url`.
///
/// # Examples
Expand Down Expand Up @@ -648,6 +681,24 @@
pub secret_store_id: String,
}

impl RequestSigning {

Check warning on line 684 in crates/trusted-server-core/src/settings.rs

View workflow job for this annotation

GitHub Actions / cargo fmt

Diff in /home/runner/work/trusted-server/trusted-server/crates/trusted-server-core/src/settings.rs
/// Reserved example store-id values from the config template, plus the
/// empty string, that must not be deployed while request signing is enabled.
pub const STORE_ID_PLACEHOLDERS: &[&str] =
&["<management-config-store-id>", "<management-secret-store-id>"];

/// Returns `true` if `store_id` is empty or a known template placeholder
/// (case-insensitive).
#[must_use]
pub fn is_placeholder_store_id(store_id: &str) -> bool {
let store_id = store_id.trim();
store_id.is_empty()
|| Self::STORE_ID_PLACEHOLDERS
.iter()
.any(|p| p.eq_ignore_ascii_case(store_id))
}
}

fn default_request_signing_enabled() -> bool {
false
}
Expand Down Expand Up @@ -2133,6 +2184,25 @@
insecure_fields.push(format!("handlers[{}].password", handler.path));
}
}
if Publisher::is_placeholder_domain(&self.publisher.domain) {
insecure_fields.push("publisher.domain".to_owned());
}
if Publisher::is_placeholder_cookie_domain(&self.publisher.cookie_domain) {
insecure_fields.push("publisher.cookie_domain".to_owned());
}
if Publisher::is_placeholder_origin_url(&self.publisher.origin_url) {
insecure_fields.push("publisher.origin_url".to_owned());
}
if let Some(request_signing) = &self.request_signing {
if request_signing.enabled {
if RequestSigning::is_placeholder_store_id(&request_signing.config_store_id) {
insecure_fields.push("request_signing.config_store_id".to_owned());
}
if RequestSigning::is_placeholder_store_id(&request_signing.secret_store_id) {
insecure_fields.push("request_signing.secret_store_id".to_owned());
}
}
}

if insecure_fields.is_empty() {
return Ok(());
Expand Down
Loading
Loading