Merge pull request #610 from EdouardVanbelle/security/grants2
This commit is contained in:
@@ -135,19 +135,40 @@ async fn user_provisioning_response(
|
||||
) -> Response {
|
||||
let statuscode = if ocs_version == 1 { 100 } else { 200 };
|
||||
|
||||
// Only allow users to view their own profile, unless they are admin.
|
||||
if user.username != userid && user.role != "admin" {
|
||||
return Json(ocs_err(403, "Insufficient privileges")).into_response();
|
||||
}
|
||||
|
||||
// AuthZ audit #11 (2026-07-12): the pre-fix path here rolled its
|
||||
// own gate ("caller is `userid`, else must be admin") and then
|
||||
// called bare `get_user_by_username` — bypassing every visibility
|
||||
// rule the id-keyed `/api/users/{id}` endpoint enforces. Cross-user
|
||||
// probes returned 403 (leaking existence via the differential vs a
|
||||
// genuine 404 for missing users); admins bypassed
|
||||
// `expose_system_users`; no audit line ever fired.
|
||||
//
|
||||
// Now routing through `get_user_profile_by_username_with_perms`,
|
||||
// which delegates to the same visibility engine as the REST
|
||||
// endpoint (self / shared-grant / expose_system_users / admin
|
||||
// paths, all audit-logged on denial). The OCS wire shape stays
|
||||
// `ocs_err(404, ...)` for every denied case — the NC client can't
|
||||
// tell "no such user" from "you can't see this user" from "you're
|
||||
// not admin" apart, which is the anti-enum invariant.
|
||||
let auth_service = match state.auth_service.as_ref() {
|
||||
Some(svc) => &svc.auth_application_service,
|
||||
None => {
|
||||
return Json(ocs_err(997, "Authentication not configured")).into_response();
|
||||
}
|
||||
};
|
||||
let Some(pool) = state.db_pool.as_ref() else {
|
||||
return Json(ocs_err(997, "Database pool not available")).into_response();
|
||||
};
|
||||
|
||||
let user_dto = match auth_service.get_user_by_username(&userid).await {
|
||||
let user_dto = match auth_service
|
||||
.get_user_profile_by_username_with_perms(
|
||||
user.id,
|
||||
&userid,
|
||||
state.core.config.features.expose_system_users,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
return Json(ocs_err(404, "User not found")).into_response();
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
|
||||
use crate::application::ports::file_ports::FileUploadUseCase;
|
||||
use crate::application::ports::storage_ports::StorageUsagePort;
|
||||
use crate::common::di::AppState;
|
||||
use crate::common::mime_detect::filename_from_path;
|
||||
@@ -402,8 +402,6 @@ async fn handle_assemble(
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list chunks: {}", e)))?;
|
||||
|
||||
let upload_service = &state.applications.file_upload_service;
|
||||
let file_service = &state.applications.file_retrieval_service;
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
// Path-based lookups below scope by `drive_id`. The NC session's
|
||||
// chroot is always populated for path-scoped handlers (see
|
||||
@@ -433,64 +431,41 @@ async fn handle_assemble(
|
||||
.await?;
|
||||
let content_type = ingested.content_type.clone();
|
||||
|
||||
// Check if file exists (update vs create).
|
||||
let existing = file_service
|
||||
.get_file_by_path(&internal_path, drive_id)
|
||||
.await;
|
||||
|
||||
let etag: Option<String> = if existing.is_ok() {
|
||||
let dto = upload_service
|
||||
.update_file_streaming_with_perms(
|
||||
&internal_path,
|
||||
drive_id,
|
||||
ingested.stored(),
|
||||
&content_type,
|
||||
oc_mtime,
|
||||
user.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to update file: {}", e)))?;
|
||||
|
||||
Some(dto.etag)
|
||||
} else {
|
||||
// New-file branch: resolve the parent folder by path and register
|
||||
// the file row against the already-ingested blob.
|
||||
let (parent_sub, filename) = match dest_subpath.rsplit_once('/') {
|
||||
Some((p, n)) => (p, n),
|
||||
None => ("", dest_subpath.as_str()),
|
||||
};
|
||||
let parent_internal =
|
||||
crate::interfaces::nextcloud::webdav_handler::nc_to_internal_path(chroot, parent_sub)?;
|
||||
let parent_internal = parent_internal.trim_end_matches('/');
|
||||
|
||||
use crate::application::ports::folder_ports::FolderUseCase;
|
||||
let parent_folder = match folder_service
|
||||
.get_folder_by_path(parent_internal, drive_id)
|
||||
.await
|
||||
{
|
||||
Ok(folder) => folder,
|
||||
Err(e) => {
|
||||
discard_ingested(&state.core.dedup_service, &ingested).await;
|
||||
return Err(AppError::internal_error(format!(
|
||||
"Parent folder lookup failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let dto = upload_service
|
||||
.upload_file_streaming(
|
||||
filename.to_string(),
|
||||
Some(parent_folder.id),
|
||||
content_type.to_string(),
|
||||
ingested.stored(),
|
||||
user.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to create file: {}", e)))?;
|
||||
|
||||
Some(dto.etag)
|
||||
// AuthZ audit #12 (2026-07-12): the previous shape branched on
|
||||
// file existence — `update_file_streaming_with_perms` on the
|
||||
// overwrite path (correct), plain `upload_file_streaming` on
|
||||
// the create path (NO `authz.require`). Viewer/Commenter on a
|
||||
// shared drive could MKCOL → PUT chunks → MOVE and land a
|
||||
// brand-new file, skipping the `Create`-on-parent-folder gate.
|
||||
//
|
||||
// `update_file_streaming_with_perms` handles both branches
|
||||
// atomically: `Update` on the existing file OR `Create` on the
|
||||
// parent folder / drive root (per the service's own internal
|
||||
// fork). Funneling everything through the one method also
|
||||
// deletes the duplicated parent-folder lookup that used to
|
||||
// live here.
|
||||
//
|
||||
// AuthZ audit #2 (2026-07-12): route DomainError through
|
||||
// `AppError::from` so authz denials keep the graduated 403/404
|
||||
// shape instead of collapsing into 500.
|
||||
let dto = match upload_service
|
||||
.update_file_streaming_with_perms(
|
||||
&internal_path,
|
||||
drive_id,
|
||||
ingested.stored(),
|
||||
&content_type,
|
||||
oc_mtime,
|
||||
user.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(dto) => dto,
|
||||
Err(e) => {
|
||||
discard_ingested(&state.core.dedup_service, &ingested).await;
|
||||
return Err(AppError::from(e));
|
||||
}
|
||||
};
|
||||
let etag: Option<String> = Some(dto.etag);
|
||||
|
||||
// Cleanup session.
|
||||
let _ = nc.chunked_uploads.cleanup(&user.username, upload_id).await;
|
||||
|
||||
Reference in New Issue
Block a user