feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
@@ -3,7 +3,7 @@ use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
use aether_data_contracts::repository::auth_modules::*;
use aether_data_contracts::DataLayerError;
use aether_data_query::{push_eq, push_limit, WhereClause};
use aether_data_query::{push_eq, WhereClause};
use crate::error::SqlxResultExt;
@@ -34,7 +34,37 @@ SELECT
FROM ldap_configs
"#;
const UPDATE_LDAP_CONFIG_SQL: &str = r#"
const UPDATE_LDAP_CONFIG_PRESERVE_PASSWORD_SQL: &str = r#"
UPDATE ldap_configs
SET
server_url = $1,
bind_dn = $2,
base_dn = $3,
user_search_filter = $4,
username_attr = $5,
email_attr = $6,
display_name_attr = $7,
is_enabled = $8,
is_exclusive = $9,
use_starttls = $10,
connect_timeout = $11,
updated_at = NOW()
WHERE singleton_key = 1
AND server_url IS NOT DISTINCT FROM $12
AND bind_dn IS NOT DISTINCT FROM $13
AND bind_password_encrypted IS NOT DISTINCT FROM $14
AND base_dn IS NOT DISTINCT FROM $15
AND user_search_filter IS NOT DISTINCT FROM $16
AND username_attr IS NOT DISTINCT FROM $17
AND email_attr IS NOT DISTINCT FROM $18
AND display_name_attr IS NOT DISTINCT FROM $19
AND is_enabled IS NOT DISTINCT FROM $20
AND is_exclusive IS NOT DISTINCT FROM $21
AND use_starttls IS NOT DISTINCT FROM $22
AND connect_timeout IS NOT DISTINCT FROM $23
"#;
const UPDATE_LDAP_CONFIG_REPLACE_PASSWORD_SQL: &str = r#"
UPDATE ldap_configs
SET
server_url = $1,
@@ -50,29 +80,24 @@ SET
use_starttls = $11,
connect_timeout = $12,
updated_at = NOW()
WHERE id = (
SELECT id
FROM ldap_configs
ORDER BY id ASC
LIMIT 1
)
RETURNING
server_url,
bind_dn,
bind_password_encrypted,
base_dn,
user_search_filter,
username_attr,
email_attr,
display_name_attr,
is_enabled,
is_exclusive,
use_starttls,
connect_timeout
WHERE singleton_key = 1
AND server_url IS NOT DISTINCT FROM $13
AND bind_dn IS NOT DISTINCT FROM $14
AND bind_password_encrypted IS NOT DISTINCT FROM $15
AND base_dn IS NOT DISTINCT FROM $16
AND user_search_filter IS NOT DISTINCT FROM $17
AND username_attr IS NOT DISTINCT FROM $18
AND email_attr IS NOT DISTINCT FROM $19
AND display_name_attr IS NOT DISTINCT FROM $20
AND is_enabled IS NOT DISTINCT FROM $21
AND is_exclusive IS NOT DISTINCT FROM $22
AND use_starttls IS NOT DISTINCT FROM $23
AND connect_timeout IS NOT DISTINCT FROM $24
"#;
const INSERT_LDAP_CONFIG_SQL: &str = r#"
INSERT INTO ldap_configs (
singleton_key,
server_url,
bind_dn,
bind_password_encrypted,
@@ -89,6 +114,7 @@ INSERT INTO ldap_configs (
updated_at
)
VALUES (
1,
$1,
$2,
$3,
@@ -104,19 +130,6 @@ VALUES (
NOW(),
NOW()
)
RETURNING
server_url,
bind_dn,
bind_password_encrypted,
base_dn,
user_search_filter,
username_attr,
email_attr,
display_name_attr,
is_enabled,
is_exclusive,
use_starttls,
connect_timeout
"#;
#[derive(Debug, Clone)]
@@ -154,8 +167,7 @@ async fn list_enabled_oauth_providers(
async fn get_ldap_config(pool: &PgPool) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
let mut builder = QueryBuilder::<Postgres>::new(LDAP_CONFIG_COLUMNS);
builder.push(" ORDER BY id ASC");
push_limit(&mut builder, 1);
builder.push(" WHERE singleton_key = 1");
let row = builder
.build()
.fetch_optional(pool)
@@ -192,48 +204,203 @@ impl AuthModuleReadRepository for SqlxAuthModuleRepository {
#[async_trait]
impl AuthModuleWriteRepository for SqlxAuthModuleRepository {
async fn upsert_ldap_config(
async fn compare_and_swap_ldap_config(
&self,
config: &StoredLdapModuleConfig,
) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
let updated = sqlx::query(UPDATE_LDAP_CONFIG_SQL)
.bind(&config.server_url)
.bind(&config.bind_dn)
.bind(config.bind_password_encrypted.as_deref())
.bind(&config.base_dn)
.bind(config.user_search_filter.as_deref())
.bind(config.username_attr.as_deref())
.bind(config.email_attr.as_deref())
.bind(config.display_name_attr.as_deref())
.bind(config.is_enabled)
.bind(config.is_exclusive)
.bind(config.use_starttls)
.bind(config.connect_timeout)
.fetch_optional(&self.pool)
.await
.map_postgres_err()?;
if let Some(row) = updated.as_ref() {
return map_ldap_row(row).map(Some);
}
expected: Option<&StoredLdapModuleConfig>,
replacement: &StoredLdapModuleConfig,
bind_password_update: &LdapBindPasswordUpdate,
) -> Result<CompareAndSwapLdapConfigResult, DataLayerError> {
let persisted =
ldap_config_after_password_update(expected, replacement, bind_password_update)?;
let inserted = sqlx::query(INSERT_LDAP_CONFIG_SQL)
.bind(&config.server_url)
.bind(&config.bind_dn)
.bind(config.bind_password_encrypted.as_deref())
.bind(&config.base_dn)
.bind(config.user_search_filter.as_deref())
.bind(config.username_attr.as_deref())
.bind(config.email_attr.as_deref())
.bind(config.display_name_attr.as_deref())
.bind(config.is_enabled)
.bind(config.is_exclusive)
.bind(config.use_starttls)
.bind(config.connect_timeout)
.fetch_optional(&self.pool)
.await
.map_postgres_err()?;
inserted.as_ref().map(map_ldap_row).transpose()
let Some(expected) = expected else {
let insert = sqlx::query(INSERT_LDAP_CONFIG_SQL)
.bind(&persisted.server_url)
.bind(&persisted.bind_dn)
.bind(persisted.bind_password_encrypted.as_deref())
.bind(&persisted.base_dn)
.bind(persisted.user_search_filter.as_deref())
.bind(persisted.username_attr.as_deref())
.bind(persisted.email_attr.as_deref())
.bind(persisted.display_name_attr.as_deref())
.bind(persisted.is_enabled)
.bind(persisted.is_exclusive)
.bind(persisted.use_starttls)
.bind(persisted.connect_timeout)
.execute(&self.pool)
.await;
return match insert {
Ok(result) if result.rows_affected() == 1 => {
Ok(CompareAndSwapLdapConfigResult::Applied(persisted))
}
Ok(_) => Ok(CompareAndSwapLdapConfigResult::Conflict),
Err(error)
if error
.as_database_error()
.is_some_and(|error| error.is_unique_violation()) =>
{
Ok(CompareAndSwapLdapConfigResult::Conflict)
}
Err(error) => Err(crate::error::postgres_error(error)),
};
};
let rows_affected = match bind_password_update {
LdapBindPasswordUpdate::Preserve => {
sqlx::query(UPDATE_LDAP_CONFIG_PRESERVE_PASSWORD_SQL)
.bind(&replacement.server_url)
.bind(&replacement.bind_dn)
.bind(&replacement.base_dn)
.bind(replacement.user_search_filter.as_deref())
.bind(replacement.username_attr.as_deref())
.bind(replacement.email_attr.as_deref())
.bind(replacement.display_name_attr.as_deref())
.bind(replacement.is_enabled)
.bind(replacement.is_exclusive)
.bind(replacement.use_starttls)
.bind(replacement.connect_timeout)
.bind(&expected.server_url)
.bind(&expected.bind_dn)
.bind(expected.bind_password_encrypted.as_deref())
.bind(&expected.base_dn)
.bind(expected.user_search_filter.as_deref())
.bind(expected.username_attr.as_deref())
.bind(expected.email_attr.as_deref())
.bind(expected.display_name_attr.as_deref())
.bind(expected.is_enabled)
.bind(expected.is_exclusive)
.bind(expected.use_starttls)
.bind(expected.connect_timeout)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected()
}
LdapBindPasswordUpdate::Set(_) | LdapBindPasswordUpdate::Clear => {
sqlx::query(UPDATE_LDAP_CONFIG_REPLACE_PASSWORD_SQL)
.bind(&replacement.server_url)
.bind(&replacement.bind_dn)
.bind(persisted.bind_password_encrypted.as_deref())
.bind(&replacement.base_dn)
.bind(replacement.user_search_filter.as_deref())
.bind(replacement.username_attr.as_deref())
.bind(replacement.email_attr.as_deref())
.bind(replacement.display_name_attr.as_deref())
.bind(replacement.is_enabled)
.bind(replacement.is_exclusive)
.bind(replacement.use_starttls)
.bind(replacement.connect_timeout)
.bind(&expected.server_url)
.bind(&expected.bind_dn)
.bind(expected.bind_password_encrypted.as_deref())
.bind(&expected.base_dn)
.bind(expected.user_search_filter.as_deref())
.bind(expected.username_attr.as_deref())
.bind(expected.email_attr.as_deref())
.bind(expected.display_name_attr.as_deref())
.bind(expected.is_enabled)
.bind(expected.is_exclusive)
.bind(expected.use_starttls)
.bind(expected.connect_timeout)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected()
}
};
if rows_affected == 1 {
Ok(CompareAndSwapLdapConfigResult::Applied(persisted))
} else {
Ok(CompareAndSwapLdapConfigResult::Conflict)
}
}
async fn delete_ldap_config_if_matches(
&self,
expected: &StoredLdapModuleConfig,
) -> Result<bool, DataLayerError> {
let rows_affected = sqlx::query(
r#"
DELETE FROM ldap_configs
WHERE singleton_key = 1
AND server_url IS NOT DISTINCT FROM $1
AND bind_dn IS NOT DISTINCT FROM $2
AND bind_password_encrypted IS NOT DISTINCT FROM $3
AND base_dn IS NOT DISTINCT FROM $4
AND user_search_filter IS NOT DISTINCT FROM $5
AND username_attr IS NOT DISTINCT FROM $6
AND email_attr IS NOT DISTINCT FROM $7
AND display_name_attr IS NOT DISTINCT FROM $8
AND is_enabled IS NOT DISTINCT FROM $9
AND is_exclusive IS NOT DISTINCT FROM $10
AND use_starttls IS NOT DISTINCT FROM $11
AND connect_timeout IS NOT DISTINCT FROM $12
"#,
)
.bind(&expected.server_url)
.bind(&expected.bind_dn)
.bind(expected.bind_password_encrypted.as_deref())
.bind(&expected.base_dn)
.bind(expected.user_search_filter.as_deref())
.bind(expected.username_attr.as_deref())
.bind(expected.email_attr.as_deref())
.bind(expected.display_name_attr.as_deref())
.bind(expected.is_enabled)
.bind(expected.is_exclusive)
.bind(expected.use_starttls)
.bind(expected.connect_timeout)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
async fn compare_and_swap_ldap_bind_password(
&self,
expected: &str,
replacement: &str,
) -> Result<bool, DataLayerError> {
let rows_affected = sqlx::query(
r#"
UPDATE ldap_configs
SET bind_password_encrypted = $1, updated_at = NOW()
WHERE singleton_key = 1
AND bind_password_encrypted = $2
"#,
)
.bind(replacement)
.bind(expected)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
}
fn ldap_config_after_password_update(
expected: Option<&StoredLdapModuleConfig>,
replacement: &StoredLdapModuleConfig,
bind_password_update: &LdapBindPasswordUpdate,
) -> Result<StoredLdapModuleConfig, DataLayerError> {
let bind_password_encrypted = match bind_password_update {
LdapBindPasswordUpdate::Preserve => expected
.ok_or_else(|| {
DataLayerError::InvalidConfiguration(
"LDAP bind password cannot be preserved while creating the singleton"
.to_string(),
)
})?
.bind_password_encrypted
.clone(),
LdapBindPasswordUpdate::Set(ciphertext) => Some(ciphertext.clone()),
LdapBindPasswordUpdate::Clear => None,
};
Ok(StoredLdapModuleConfig {
bind_password_encrypted,
..replacement.clone()
})
}
fn map_oauth_row(row: &PgRow) -> Result<StoredOAuthProviderModuleConfig, DataLayerError> {