feat(drive): add policy forbid_public_links
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,10 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
get(drive_handler::list_drives).post(drive_handler::create_drive),
|
||||
)
|
||||
.route("/{id}", axum::routing::delete(drive_handler::delete_drive))
|
||||
.route(
|
||||
"/{id}/policies",
|
||||
patch(drive_handler::update_drive_policies),
|
||||
)
|
||||
.route(
|
||||
"/{id}/members",
|
||||
get(drive_handler::list_drive_members).post(drive_handler::add_drive_member),
|
||||
|
||||
Reference in New Issue
Block a user