Implement replay-safe authorized public HTTP API
This commit is contained in:
parent
791e00f4c0
commit
4c93c25692
1 changed files with 3940 additions and 345 deletions
|
|
@ -2,7 +2,7 @@ use std::time::Duration;
|
|||
|
||||
use futures_util::TryStreamExt;
|
||||
use remora_domain::{BucketName, OpaqueId, Target, TargetPrefix, TargetStoreId};
|
||||
use remora_durable::enqueue;
|
||||
use remora_durable::{PgTx, enqueue};
|
||||
use remora_identity::{
|
||||
AuthorizedResource, ManagementOperation, Principal, ResourceVisibility, resource_visibility,
|
||||
};
|
||||
|
|
@ -52,14 +52,23 @@ impl DeletionService {
|
|||
job_id: &OpaqueId,
|
||||
) -> Result<LifecycleMutation, DeletionError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let row = lock_job(&mut tx, job_id).await?;
|
||||
let outcome = Self::soft_delete_in_tx(&mut tx, principal, job_id).await?;
|
||||
tx.commit().await?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub async fn soft_delete_in_tx(
|
||||
tx: &mut PgTx<'_>,
|
||||
principal: &Principal,
|
||||
job_id: &OpaqueId,
|
||||
) -> Result<LifecycleMutation, DeletionError> {
|
||||
let row = lock_job(tx, job_id).await?;
|
||||
authorize(principal, &row, ManagementOperation::SoftDelete)?;
|
||||
require_terminal(&row.phase)?;
|
||||
if row.deletion_operation_id.is_some() {
|
||||
return Err(DeletionError::HardDeleteStarted);
|
||||
}
|
||||
if row.lifecycle == "soft_deleted" {
|
||||
tx.commit().await?;
|
||||
return Ok(LifecycleMutation::Unchanged);
|
||||
}
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
|
@ -72,8 +81,7 @@ impl DeletionService {
|
|||
.bind(now)
|
||||
.execute(tx.as_mut())
|
||||
.await?;
|
||||
enqueue(&mut tx, "job.updated", json!({ "job_id": job_id })).await?;
|
||||
tx.commit().await?;
|
||||
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
|
||||
Ok(LifecycleMutation::Changed)
|
||||
}
|
||||
|
||||
|
|
@ -83,14 +91,23 @@ impl DeletionService {
|
|||
job_id: &OpaqueId,
|
||||
) -> Result<LifecycleMutation, DeletionError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let row = lock_job(&mut tx, job_id).await?;
|
||||
let outcome = Self::restore_in_tx(&mut tx, principal, job_id).await?;
|
||||
tx.commit().await?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub async fn restore_in_tx(
|
||||
tx: &mut PgTx<'_>,
|
||||
principal: &Principal,
|
||||
job_id: &OpaqueId,
|
||||
) -> Result<LifecycleMutation, DeletionError> {
|
||||
let row = lock_job(tx, job_id).await?;
|
||||
authorize(principal, &row, ManagementOperation::Restore)?;
|
||||
require_terminal(&row.phase)?;
|
||||
if row.deletion_operation_id.is_some() {
|
||||
return Err(DeletionError::HardDeleteStarted);
|
||||
}
|
||||
if row.lifecycle == "visible" {
|
||||
tx.commit().await?;
|
||||
return Ok(LifecycleMutation::Unchanged);
|
||||
}
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
|
@ -103,8 +120,7 @@ impl DeletionService {
|
|||
.bind(now)
|
||||
.execute(tx.as_mut())
|
||||
.await?;
|
||||
enqueue(&mut tx, "job.updated", json!({ "job_id": job_id })).await?;
|
||||
tx.commit().await?;
|
||||
enqueue(tx, "job.updated", json!({ "job_id": job_id })).await?;
|
||||
Ok(LifecycleMutation::Changed)
|
||||
}
|
||||
|
||||
|
|
@ -113,21 +129,30 @@ impl DeletionService {
|
|||
principal: &Principal,
|
||||
job_id: &OpaqueId,
|
||||
) -> Result<HardDeleteOutcome, DeletionError> {
|
||||
let operation_id = new_id('d')?;
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let row = lock_job(&mut tx, job_id).await?;
|
||||
let outcome = Self::hard_delete_in_tx(&mut tx, principal, job_id).await?;
|
||||
tx.commit().await?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub async fn hard_delete_in_tx(
|
||||
tx: &mut PgTx<'_>,
|
||||
principal: &Principal,
|
||||
job_id: &OpaqueId,
|
||||
) -> Result<HardDeleteOutcome, DeletionError> {
|
||||
let operation_id = new_id('d')?;
|
||||
let row = lock_job(tx, job_id).await?;
|
||||
authorize(principal, &row, ManagementOperation::HardDelete)?;
|
||||
require_terminal(&row.phase)?;
|
||||
if let Some(existing) = row.deletion_operation_id {
|
||||
let operation = load_operation_in_tx(&mut tx, &existing).await?;
|
||||
tx.commit().await?;
|
||||
let operation = load_operation_in_tx(tx, &existing).await?;
|
||||
return Ok(HardDeleteOutcome {
|
||||
mutation: LifecycleMutation::Unchanged,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
validate_committed_inventory(&mut tx, &row).await?;
|
||||
validate_deletion_identities(&mut tx, &row).await?;
|
||||
validate_committed_inventory(tx, &row).await?;
|
||||
validate_deletion_identities(tx, &row).await?;
|
||||
let now = OffsetDateTime::now_utc();
|
||||
sqlx::query(
|
||||
"INSERT INTO deletion_operations (
|
||||
|
|
@ -150,16 +175,15 @@ impl DeletionService {
|
|||
.bind(now)
|
||||
.execute(tx.as_mut())
|
||||
.await?;
|
||||
materialize_items(&mut tx, job_id, &operation_id, now).await?;
|
||||
finalize_if_empty(&mut tx, job_id, &operation_id, now).await?;
|
||||
materialize_items(tx, job_id, &operation_id, now).await?;
|
||||
finalize_if_empty(tx, job_id, &operation_id, now).await?;
|
||||
enqueue(
|
||||
&mut tx,
|
||||
tx,
|
||||
"job.updated",
|
||||
json!({ "job_id": job_id, "deletion_operation_id": operation_id }),
|
||||
)
|
||||
.await?;
|
||||
let operation = load_operation_in_tx(&mut tx, operation_id.as_str()).await?;
|
||||
tx.commit().await?;
|
||||
let operation = load_operation_in_tx(tx, operation_id.as_str()).await?;
|
||||
Ok(HardDeleteOutcome {
|
||||
mutation: LifecycleMutation::Changed,
|
||||
operation,
|
||||
|
|
|
|||
Loading…
Reference in a new issue