mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
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:
@@ -101,10 +101,18 @@ fn find_by_id_sql() -> String {
|
||||
select_video_task_sql("WHERE id = $1\nLIMIT 1")
|
||||
}
|
||||
|
||||
fn find_by_id_for_user_sql() -> String {
|
||||
select_video_task_sql("WHERE id = $1 AND user_id = $2\nLIMIT 1")
|
||||
}
|
||||
|
||||
fn find_by_short_id_sql() -> String {
|
||||
select_video_task_sql("WHERE short_id = $1\nLIMIT 1")
|
||||
}
|
||||
|
||||
fn find_by_short_id_for_user_sql() -> String {
|
||||
select_video_task_sql("WHERE short_id = $1 AND user_id = $2\nLIMIT 1")
|
||||
}
|
||||
|
||||
fn find_by_user_external_sql() -> String {
|
||||
select_video_task_sql("WHERE user_id = $1 AND external_task_id = $2\nLIMIT 1")
|
||||
}
|
||||
@@ -302,10 +310,26 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
error_code = EXCLUDED.error_code,
|
||||
error_message = EXCLUDED.error_message,
|
||||
request_metadata = EXCLUDED.request_metadata,
|
||||
created_at = EXCLUDED.created_at,
|
||||
created_at = COALESCE(video_tasks.created_at, EXCLUDED.created_at),
|
||||
submitted_at = EXCLUDED.submitted_at,
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE video_tasks.short_id IS NOT DISTINCT FROM EXCLUDED.short_id
|
||||
AND video_tasks.request_id IS NOT DISTINCT FROM EXCLUDED.request_id
|
||||
AND video_tasks.user_id IS NOT DISTINCT FROM EXCLUDED.user_id
|
||||
AND video_tasks.api_key_id IS NOT DISTINCT FROM EXCLUDED.api_key_id
|
||||
AND video_tasks.external_task_id IS NOT DISTINCT FROM EXCLUDED.external_task_id
|
||||
AND video_tasks.provider_id IS NOT DISTINCT FROM EXCLUDED.provider_id
|
||||
AND video_tasks.endpoint_id IS NOT DISTINCT FROM EXCLUDED.endpoint_id
|
||||
AND video_tasks.key_id IS NOT DISTINCT FROM EXCLUDED.key_id
|
||||
AND video_tasks.client_api_format IS NOT DISTINCT FROM EXCLUDED.client_api_format
|
||||
AND video_tasks.provider_api_format IS NOT DISTINCT FROM EXCLUDED.provider_api_format
|
||||
AND video_tasks.format_converted IS NOT DISTINCT FROM EXCLUDED.format_converted
|
||||
AND video_tasks.model IS NOT DISTINCT FROM EXCLUDED.model
|
||||
AND video_tasks.duration_seconds IS NOT DISTINCT FROM EXCLUDED.duration_seconds
|
||||
AND video_tasks.resolution IS NOT DISTINCT FROM EXCLUDED.resolution
|
||||
AND video_tasks.aspect_ratio IS NOT DISTINCT FROM EXCLUDED.aspect_ratio
|
||||
AND video_tasks.size IS NOT DISTINCT FROM EXCLUDED.size
|
||||
RETURNING
|
||||
{columns}
|
||||
"
|
||||
@@ -348,12 +372,28 @@ fn update_if_active_sql() -> String {
|
||||
error_code = $31,
|
||||
error_message = $32,
|
||||
request_metadata = $33,
|
||||
created_at = TO_TIMESTAMP($34),
|
||||
created_at = COALESCE(created_at, TO_TIMESTAMP($34)),
|
||||
submitted_at = TO_TIMESTAMP($35),
|
||||
completed_at = TO_TIMESTAMP($36),
|
||||
updated_at = TO_TIMESTAMP($37)
|
||||
WHERE id = $1
|
||||
AND status = ANY($38)
|
||||
AND short_id IS NOT DISTINCT FROM $2
|
||||
AND request_id IS NOT DISTINCT FROM $3
|
||||
AND user_id IS NOT DISTINCT FROM $4
|
||||
AND api_key_id IS NOT DISTINCT FROM $5
|
||||
AND external_task_id IS NOT DISTINCT FROM $8
|
||||
AND provider_id IS NOT DISTINCT FROM $9
|
||||
AND endpoint_id IS NOT DISTINCT FROM $10
|
||||
AND key_id IS NOT DISTINCT FROM $11
|
||||
AND client_api_format IS NOT DISTINCT FROM $12
|
||||
AND provider_api_format IS NOT DISTINCT FROM $13
|
||||
AND format_converted IS NOT DISTINCT FROM $14
|
||||
AND model IS NOT DISTINCT FROM $15
|
||||
AND duration_seconds IS NOT DISTINCT FROM $18
|
||||
AND resolution IS NOT DISTINCT FROM $19
|
||||
AND aspect_ratio IS NOT DISTINCT FROM $20
|
||||
AND size IS NOT DISTINCT FROM $21
|
||||
RETURNING
|
||||
{columns}
|
||||
"
|
||||
@@ -390,6 +430,26 @@ impl SqlxVideoTaskRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_for_user(
|
||||
&self,
|
||||
key: VideoTaskLookupKey<'_>,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
match key {
|
||||
VideoTaskLookupKey::Id(id) => self.find_by_id_for_user(id, user_id).await,
|
||||
VideoTaskLookupKey::ShortId(short_id) => {
|
||||
self.find_by_short_id_for_user(short_id, user_id).await
|
||||
}
|
||||
VideoTaskLookupKey::UserExternal {
|
||||
user_id: lookup_user_id,
|
||||
external_task_id,
|
||||
} if lookup_user_id == user_id => {
|
||||
self.find_by_user_external(user_id, external_task_id).await
|
||||
}
|
||||
VideoTaskLookupKey::UserExternal { .. } => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, id: &str) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
let sql = find_by_id_sql();
|
||||
let row = sqlx::query(&sql)
|
||||
@@ -400,6 +460,21 @@ impl SqlxVideoTaskRepository {
|
||||
row.as_ref().map(map_video_task_row).transpose()
|
||||
}
|
||||
|
||||
pub async fn find_by_id_for_user(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
let sql = find_by_id_for_user_sql();
|
||||
let row = sqlx::query(&sql)
|
||||
.bind(id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_video_task_row).transpose()
|
||||
}
|
||||
|
||||
pub async fn find_by_short_id(
|
||||
&self,
|
||||
short_id: &str,
|
||||
@@ -413,6 +488,21 @@ impl SqlxVideoTaskRepository {
|
||||
row.as_ref().map(map_video_task_row).transpose()
|
||||
}
|
||||
|
||||
pub async fn find_by_short_id_for_user(
|
||||
&self,
|
||||
short_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
let sql = find_by_short_id_for_user_sql();
|
||||
let row = sqlx::query(&sql)
|
||||
.bind(short_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_video_task_row).transpose()
|
||||
}
|
||||
|
||||
pub async fn find_by_user_external(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -661,7 +751,12 @@ impl SqlxVideoTaskRepository {
|
||||
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid count result: {total}")))
|
||||
}
|
||||
|
||||
pub async fn upsert(&self, task: UpsertVideoTask) -> Result<StoredVideoTask, DataLayerError> {
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
mut task: UpsertVideoTask,
|
||||
) -> Result<StoredVideoTask, DataLayerError> {
|
||||
task.sanitize_for_persistence();
|
||||
let task_id = task.id.clone();
|
||||
let sql = upsert_sql();
|
||||
let row = sqlx::query(&sql)
|
||||
.bind(task.id)
|
||||
@@ -720,17 +815,23 @@ impl SqlxVideoTaskRepository {
|
||||
.bind(task.submitted_at_unix_secs.map(|value| value as f64))
|
||||
.bind(task.completed_at_unix_secs.map(|value| value as f64))
|
||||
.bind(task.updated_at_unix_secs as f64)
|
||||
.fetch_one(&self.pool)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
.map_postgres_err()?
|
||||
.ok_or_else(|| {
|
||||
DataLayerError::InvalidInput(format!(
|
||||
"video task {task_id} conflicts with persisted immutable identity"
|
||||
))
|
||||
})?;
|
||||
|
||||
map_video_task_row(&row)
|
||||
}
|
||||
|
||||
pub async fn update_if_active(
|
||||
&self,
|
||||
task: UpsertVideoTask,
|
||||
mut task: UpsertVideoTask,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
task.sanitize_for_persistence();
|
||||
let sql = update_if_active_sql();
|
||||
let row = sqlx::query(&sql)
|
||||
.bind(task.id)
|
||||
@@ -839,6 +940,14 @@ impl VideoTaskReadRepository for SqlxVideoTaskRepository {
|
||||
Self::find(self, key).await
|
||||
}
|
||||
|
||||
async fn find_for_user(
|
||||
&self,
|
||||
key: VideoTaskLookupKey<'_>,
|
||||
user_id: &str,
|
||||
) -> Result<Option<StoredVideoTask>, DataLayerError> {
|
||||
Self::find_for_user(self, key, user_id).await
|
||||
}
|
||||
|
||||
async fn list_active(&self, limit: usize) -> Result<Vec<StoredVideoTask>, DataLayerError> {
|
||||
Self::list_active(self, limit).await
|
||||
}
|
||||
@@ -1067,7 +1176,7 @@ fn map_video_task_row(row: &PgRow) -> Result<StoredVideoTask, DataLayerError> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SqlxVideoTaskRepository;
|
||||
use super::{update_if_active_sql, upsert_sql, SqlxVideoTaskRepository};
|
||||
use crate::{PostgresPoolConfig, PostgresPoolFactory};
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskLookupKey, VideoTaskQueryFilter, VideoTaskReadRepository,
|
||||
@@ -1090,6 +1199,47 @@ mod tests {
|
||||
factory.connect_lazy().expect("pool should build")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_sql_guards_every_immutable_identity_field() {
|
||||
let upsert = upsert_sql();
|
||||
let update = update_if_active_sql();
|
||||
let immutable_columns = [
|
||||
("short_id", "$2"),
|
||||
("request_id", "$3"),
|
||||
("user_id", "$4"),
|
||||
("api_key_id", "$5"),
|
||||
("external_task_id", "$8"),
|
||||
("provider_id", "$9"),
|
||||
("endpoint_id", "$10"),
|
||||
("key_id", "$11"),
|
||||
("client_api_format", "$12"),
|
||||
("provider_api_format", "$13"),
|
||||
("format_converted", "$14"),
|
||||
("model", "$15"),
|
||||
("duration_seconds", "$18"),
|
||||
("resolution", "$19"),
|
||||
("aspect_ratio", "$20"),
|
||||
("size", "$21"),
|
||||
];
|
||||
|
||||
for (column, parameter) in immutable_columns {
|
||||
assert!(
|
||||
upsert.contains(&format!(
|
||||
"video_tasks.{column} IS NOT DISTINCT FROM EXCLUDED.{column}"
|
||||
)),
|
||||
"upsert should guard {column}"
|
||||
);
|
||||
assert!(
|
||||
update.contains(&format!("{column} IS NOT DISTINCT FROM {parameter}")),
|
||||
"active update should guard {column}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
upsert.contains("created_at = COALESCE(video_tasks.created_at, EXCLUDED.created_at)")
|
||||
);
|
||||
assert!(update.contains("created_at = COALESCE(created_at, TO_TIMESTAMP($34))"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repository_constructs_from_lazy_pool() {
|
||||
let repository = SqlxVideoTaskRepository::new(build_pool());
|
||||
|
||||
Reference in New Issue
Block a user