feat(drive): add policy forbid_public_links

This commit is contained in:
Edouard Vanbelle
2026-06-26 01:32:59 +02:00
parent cfd783cbd3
commit ddb131da8b
10 changed files with 568 additions and 3 deletions
@@ -395,3 +395,89 @@ pub async fn delete_drive(
Err(e) => AppError::from(e).into_response(),
}
}
/// Body for `PATCH /api/drives/{id}/policies` (D5).
///
/// Partial merge: any field left out of the JSON keeps its current
/// JSONB value (the repo uses `policies || $partial`). Each field
/// defaults to `false` in `DrivePolicies`, but the merge is keyed on
/// presence — so omitting a field means "leave it alone", not "set
/// it to false". Clients flip a single key at a time without
/// round-tripping the whole bag.
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateDrivePoliciesDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forbid_sharing: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forbid_external_sharing: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forbid_public_links: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forbid_cross_drive_move: Option<bool>,
}
/// `PATCH /api/drives/{id}/policies` — Owner-only policy update (D5).
///
/// Caller must hold `Manage` on the drive (Owner role bundle). Personal
/// drives are eligible too — a user can disable `forbid_public_links`
/// on their own Personal drive without the membership API. Partial
/// merge into the JSONB `policies` column; the post-merge typed view
/// is returned.
///
/// Audit: emits `drive.policy_changed` with every key's post-merge
/// value (steady-state observability).
#[utoipa::path(
patch,
path = "/api/drives/{id}/policies",
params(("id" = Uuid, Path, description = "Drive UUID")),
request_body = UpdateDrivePoliciesDto,
responses(
(status = 200, description = "Policies merged"),
(status = 404, description = "Drive not found or caller lacks Manage"),
),
security(("bearerAuth" = [])),
tag = "drives"
)]
pub async fn update_drive_policies(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(drive_id): Path<Uuid>,
axum::Json(dto): axum::Json<UpdateDrivePoliciesDto>,
) -> impl IntoResponse {
// Translate the Option-per-field DTO into a serde_json partial that
// only carries the supplied keys, so the JSONB merge in
// `update_policies` skips fields the caller didn't touch. Building a
// `DrivePolicies` and serialising would lose the partial-update
// semantics (every field defaults to false → omitted vs. "set to
// false" become indistinguishable on the wire).
let mut partial_obj = serde_json::Map::new();
if let Some(v) = dto.forbid_sharing {
partial_obj.insert("forbid_sharing".into(), serde_json::Value::Bool(v));
}
if let Some(v) = dto.forbid_external_sharing {
partial_obj.insert("forbid_external_sharing".into(), serde_json::Value::Bool(v));
}
if let Some(v) = dto.forbid_public_links {
partial_obj.insert("forbid_public_links".into(), serde_json::Value::Bool(v));
}
if let Some(v) = dto.forbid_cross_drive_move {
partial_obj.insert("forbid_cross_drive_move".into(), serde_json::Value::Bool(v));
}
let partial_value = serde_json::Value::Object(partial_obj);
let partial: crate::domain::entities::drive::DrivePolicies =
match serde_json::from_value(partial_value) {
Ok(p) => p,
Err(e) => {
return AppError::bad_request(format!("invalid policy body: {e}")).into_response();
}
};
match state
.drive_management_service
.update_policies(auth_user.id, false, drive_id, partial)
.await
{
Ok(merged) => (StatusCode::OK, axum::Json(merged)).into_response(),
Err(e) => AppError::from(e).into_response(),
}
}