fix(security): V-01,V-02,V-04,V-05,V-06 - multiple vulnerability fixes
- V-01: CalDAV unbounded body limit → MAX_CALDAV_BODY = 1MB - V-02: CardDAV unbounded body limit → MAX_CARDDAV_BODY = 1MB - V-04: Batch operations without max size → MAX_BATCH_SIZE = 1000 - V-05: WOPI get_editor_url IDOR → authorize_wopi_access with ownership check - V-06: JWT secret without entropy validation → panic <16, warn 16-31, accept >=32
This commit is contained in:
+21
-1
@@ -569,10 +569,30 @@ impl AppConfig {
|
|||||||
|
|
||||||
// Auth configuration
|
// Auth configuration
|
||||||
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
|
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
|
||||||
|
// SECURITY: Validate JWT secret minimum entropy (RFC 7518 §3.2
|
||||||
|
// recommends ≥256 bits for HS256). Panic on dangerously short
|
||||||
|
// secrets, warn on sub-optimal ones.
|
||||||
|
let len = jwt_secret.len();
|
||||||
|
if config.features.enable_auth && len < 16 {
|
||||||
|
panic!(
|
||||||
|
"FATAL: OXICLOUD_JWT_SECRET is dangerously short ({} bytes). \
|
||||||
|
Minimum: 32 bytes (256 bits) for HS256. \
|
||||||
|
Generate a secure secret with: openssl rand -hex 32",
|
||||||
|
len
|
||||||
|
);
|
||||||
|
} else if config.features.enable_auth && len < 32 {
|
||||||
|
tracing::warn!("==========================================================");
|
||||||
|
tracing::warn!(
|
||||||
|
"OXICLOUD_JWT_SECRET is only {} bytes — recommended minimum is 32 (256 bits).",
|
||||||
|
len
|
||||||
|
);
|
||||||
|
tracing::warn!("Generate a stronger secret with: openssl rand -hex 32");
|
||||||
|
tracing::warn!("==========================================================");
|
||||||
|
}
|
||||||
config.auth.jwt_secret = jwt_secret;
|
config.auth.jwt_secret = jwt_secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY: Validate JWT secret when auth is enabled
|
// SECURITY: Generate ephemeral secret when none is provided
|
||||||
if config.features.enable_auth && config.auth.jwt_secret.is_empty() {
|
if config.features.enable_auth && config.auth.jwt_secret.is_empty() {
|
||||||
// Generate a random secret for this session and warn loudly
|
// Generate a random secret for this session and warn loudly
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ use crate::application::services::batch_operations::{
|
|||||||
use crate::interfaces::api::handlers::ApiResult;
|
use crate::interfaces::api::handlers::ApiResult;
|
||||||
use crate::interfaces::middleware::auth::AuthUser;
|
use crate::interfaces::middleware::auth::AuthUser;
|
||||||
|
|
||||||
|
/// Maximum number of items allowed in a single batch request.
|
||||||
|
/// Prevents fan-out amplification attacks and database connection exhaustion.
|
||||||
|
const MAX_BATCH_SIZE: usize = 1_000;
|
||||||
|
|
||||||
/// Shared state for the batch handler
|
/// Shared state for the batch handler
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BatchHandlerState {
|
pub struct BatchHandlerState {
|
||||||
@@ -143,6 +147,15 @@ pub async fn move_files_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -187,6 +200,15 @@ pub async fn copy_files_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -231,6 +253,15 @@ pub async fn delete_files_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -283,6 +314,15 @@ pub async fn delete_folders_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -335,6 +375,15 @@ pub async fn create_folders_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.folders.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.folders.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Transform the format for the service
|
// Transform the format for the service
|
||||||
let folders = request
|
let folders = request
|
||||||
@@ -386,6 +435,15 @@ pub async fn get_files_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -430,6 +488,15 @@ pub async fn get_folders_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
@@ -495,6 +562,16 @@ pub async fn trash_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
let combined_size = request.file_ids.len() + request.folder_ids.len();
|
||||||
|
if combined_size > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", combined_size, MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
let mut all_successful: Vec<String> = Vec::new();
|
let mut all_successful: Vec<String> = Vec::new();
|
||||||
let mut all_failed: Vec<FailedOperation> = Vec::new();
|
let mut all_failed: Vec<FailedOperation> = Vec::new();
|
||||||
@@ -597,6 +674,15 @@ pub async fn move_folders_batch(
|
|||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
@@ -637,6 +723,13 @@ pub async fn download_batch(
|
|||||||
"No file or folder IDs provided".to_string(),
|
"No file or folder IDs provided".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let combined_size = request.file_ids.len() + request.folder_ids.len();
|
||||||
|
if combined_size > MAX_BATCH_SIZE {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Batch size {} exceeds maximum of {}", combined_size, MAX_BATCH_SIZE),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let temp_file = state
|
let temp_file = state
|
||||||
.batch_service
|
.batch_service
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ use crate::interfaces::middleware::auth::CurrentUser;
|
|||||||
|
|
||||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||||
|
|
||||||
|
/// Maximum allowed request body size for CalDAV XML/iCal endpoints (1 MB).
|
||||||
|
/// Prevents OOM/DoS via unbounded body buffering.
|
||||||
|
const MAX_CALDAV_BODY: usize = 1_048_576;
|
||||||
|
|
||||||
/// Creates CalDAV routes with full path prefixes.
|
/// Creates CalDAV routes with full path prefixes.
|
||||||
///
|
///
|
||||||
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
||||||
@@ -182,7 +186,7 @@ async fn handle_propfind(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let calendar_service = get_calendar_service(&state)?;
|
let calendar_service = get_calendar_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CALDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -423,7 +427,7 @@ async fn handle_report(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let calendar_service = get_calendar_service(&state)?;
|
let calendar_service = get_calendar_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CALDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -498,7 +502,7 @@ async fn handle_mkcalendar(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let calendar_service = get_calendar_service(&state)?;
|
let calendar_service = get_calendar_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CALDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -551,7 +555,7 @@ async fn handle_put(
|
|||||||
|
|
||||||
let calendar_id = parts[0];
|
let calendar_id = parts[0];
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CALDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -795,7 +799,7 @@ async fn handle_proppatch(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let calendar_service = get_calendar_service(&state)?;
|
let calendar_service = get_calendar_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CALDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ use crate::interfaces::middleware::auth::CurrentUser;
|
|||||||
|
|
||||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||||
|
|
||||||
|
/// Maximum allowed request body size for CardDAV XML/vCard endpoints (1 MB).
|
||||||
|
/// Prevents OOM/DoS via unbounded body buffering.
|
||||||
|
const MAX_CARDDAV_BODY: usize = 1_048_576;
|
||||||
|
|
||||||
/// Creates CardDAV routes with full path prefixes.
|
/// Creates CardDAV routes with full path prefixes.
|
||||||
///
|
///
|
||||||
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
||||||
@@ -181,7 +185,7 @@ async fn handle_propfind(
|
|||||||
let addressbook_service = get_addressbook_service(&state)?;
|
let addressbook_service = get_addressbook_service(&state)?;
|
||||||
let contact_svc = get_contact_service(&state)?;
|
let contact_svc = get_contact_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CARDDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -312,7 +316,7 @@ async fn handle_report(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let contact_svc = get_contact_service(&state)?;
|
let contact_svc = get_contact_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CARDDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -381,7 +385,7 @@ async fn handle_mkcol(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let addressbook_service = get_addressbook_service(&state)?;
|
let addressbook_service = get_addressbook_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CARDDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -435,7 +439,7 @@ async fn handle_put(
|
|||||||
|
|
||||||
let address_book_id = parts[0];
|
let address_book_id = parts[0];
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CARDDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
@@ -628,7 +632,7 @@ async fn handle_proppatch(
|
|||||||
let user = extract_user(&req)?;
|
let user = extract_user(&req)?;
|
||||||
let addressbook_service = get_addressbook_service(&state)?;
|
let addressbook_service = get_addressbook_service(&state)?;
|
||||||
|
|
||||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
let body_bytes = body::to_bytes(req.into_body(), MAX_CARDDAV_BODY)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
|
|||||||
@@ -386,6 +386,28 @@ pub struct EditorUrlResponse {
|
|||||||
pub access_token_ttl: i64,
|
pub access_token_ttl: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines if `caller_id` can access `file_id` and with what permissions.
|
||||||
|
///
|
||||||
|
/// Uses the SQL-level ownership check (`get_file_owned`) so that files
|
||||||
|
/// belonging to other users — or non-existent files — both return `NOT_FOUND`,
|
||||||
|
/// avoiding existence-leak oracles.
|
||||||
|
///
|
||||||
|
/// Returns `(FileDto, can_write)` on success.
|
||||||
|
async fn authorize_wopi_access<S: FileRetrievalUseCase>(
|
||||||
|
file_retrieval: &S,
|
||||||
|
file_id: &str,
|
||||||
|
caller_id: &str,
|
||||||
|
requested_action: &str,
|
||||||
|
) -> Result<(crate::application::dtos::file_dto::FileDto, bool), StatusCode> {
|
||||||
|
let file = file_retrieval
|
||||||
|
.get_file_owned(file_id, caller_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
// Owner verified — grant write unless explicitly requesting view-only.
|
||||||
|
let can_write = requested_action != "view";
|
||||||
|
Ok((file, can_write))
|
||||||
|
}
|
||||||
|
|
||||||
/// GET /api/wopi/editor-url — Returns the editor iframe URL + WOPI token.
|
/// GET /api/wopi/editor-url — Returns the editor iframe URL + WOPI token.
|
||||||
///
|
///
|
||||||
/// This endpoint is behind normal auth middleware. The authenticated user
|
/// This endpoint is behind normal auth middleware. The authenticated user
|
||||||
@@ -399,16 +421,17 @@ pub async fn get_editor_url(
|
|||||||
Query(params): Query<EditorUrlParams>,
|
Query(params): Query<EditorUrlParams>,
|
||||||
State(state): State<WopiState>,
|
State(state): State<WopiState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// Get file info to determine extension
|
// Verify the caller owns the file (SQL-level check, no existence leak).
|
||||||
let file = match state
|
let (file, can_write) = match authorize_wopi_access(
|
||||||
.app_state
|
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||||
.applications
|
¶ms.file_id,
|
||||||
.file_retrieval_service
|
&user_id,
|
||||||
.get_file(¶ms.file_id)
|
¶ms.action,
|
||||||
.await
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(f) => f,
|
Ok(result) => result,
|
||||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
Err(status) => return status.into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract extension from filename
|
// Extract extension from filename
|
||||||
@@ -437,13 +460,6 @@ pub async fn get_editor_url(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine write permission: owner can write, others read-only.
|
|
||||||
// If no owner_id on the file, default to allowing write.
|
|
||||||
let can_write = match &file.owner_id {
|
|
||||||
Some(owner) => owner == &user_id,
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate WOPI access token
|
// Generate WOPI access token
|
||||||
let (access_token, access_token_ttl) =
|
let (access_token, access_token_ttl) =
|
||||||
match state
|
match state
|
||||||
@@ -485,20 +501,22 @@ async fn host_page(
|
|||||||
return StatusCode::UNAUTHORIZED.into_response();
|
return StatusCode::UNAUTHORIZED.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file info for extension
|
// Re-verify ownership even though the token was valid — defence in depth.
|
||||||
let file = match state
|
let requested_action = if claims.can_write { "edit" } else { "view" };
|
||||||
.app_state
|
let file = match authorize_wopi_access(
|
||||||
.applications
|
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||||
.file_retrieval_service
|
&file_id,
|
||||||
.get_file(&file_id)
|
&claims.sub,
|
||||||
.await
|
requested_action,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(f) => f,
|
Ok((f, _)) => f,
|
||||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
Err(status) => return status.into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let extension = file.name.rsplit('.').next().unwrap_or("").to_lowercase();
|
let extension = file.name.rsplit('.').next().unwrap_or("").to_lowercase();
|
||||||
let action = if claims.can_write { "edit" } else { "view" };
|
let action = requested_action;
|
||||||
let wopi_src = format!("{}/wopi/files/{}", state.wopi_base_url, file_id);
|
let wopi_src = format!("{}/wopi/files/{}", state.wopi_base_url, file_id);
|
||||||
|
|
||||||
let editor_url = match state
|
let editor_url = match state
|
||||||
|
|||||||
Reference in New Issue
Block a user