security(music): ensure read permission via authz
This commit is contained in:
@@ -5,17 +5,31 @@ use crate::application::dtos::playlist_dto::{
|
||||
AddTracksDto, AudioMetadataDto, CreatePlaylistDto, PlaylistDto, PlaylistItemDto,
|
||||
PlaylistQueryDto, PlaylistShareInfoDto, ReorderTracksDto, SharePlaylistDto, UpdatePlaylistDto,
|
||||
};
|
||||
use crate::application::ports::authorization_ports::AuthorizationEngine;
|
||||
use crate::application::ports::music_ports::{MusicStoragePort, MusicUseCase};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::services::authorization::{Permission, Resource, Subject};
|
||||
use crate::infrastructure::adapters::music_storage_adapter::MusicStorageAdapter;
|
||||
use crate::infrastructure::services::pg_acl_engine::PgAclEngine;
|
||||
|
||||
pub struct MusicService {
|
||||
storage: Arc<MusicStorageAdapter>,
|
||||
/// ReBAC engine — Round 1 fix from `docs/plan/authz_audit/`.
|
||||
/// Currently used ONLY by `get_audio_metadata` to close the
|
||||
/// cross-tenant IDOR (`_user_id: Uuid` was deliberately unused).
|
||||
/// The full engine rewrite (Round 3 — `Resource::Playlist` +
|
||||
/// authz.require on every playlist verb) is a separate PR;
|
||||
/// don't extend the bespoke `user_has_access` / `user_can_write`
|
||||
/// pattern to new methods, use `require` here instead.
|
||||
authorization: Arc<PgAclEngine>,
|
||||
}
|
||||
|
||||
impl MusicService {
|
||||
pub fn new(storage: Arc<MusicStorageAdapter>) -> Self {
|
||||
Self { storage }
|
||||
pub fn new(storage: Arc<MusicStorageAdapter>, authorization: Arc<PgAclEngine>) -> Self {
|
||||
Self {
|
||||
storage,
|
||||
authorization,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,10 +389,24 @@ impl MusicUseCase for MusicService {
|
||||
async fn get_audio_metadata(
|
||||
&self,
|
||||
file_id: &str,
|
||||
_user_id: Uuid,
|
||||
caller_id: Uuid,
|
||||
) -> Result<Option<AudioMetadataDto>, DomainError> {
|
||||
let file_uuid = Uuid::parse_str(file_id)
|
||||
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Music", "Invalid file ID"))?;
|
||||
// AuthZ pre-read: caller must have `Read` on the underlying
|
||||
// audio file. Before this check the endpoint returned
|
||||
// metadata for any known file id (cross-tenant IDOR — the
|
||||
// `_user_id` parameter was deliberately unused). `require`
|
||||
// returns 404 on denial to match the anti-enum shape used
|
||||
// everywhere else. Post-Drive AuthZ audit fix (Round 1
|
||||
// BLOCKER — `docs/plan/authz_audit/rest_storage.md`).
|
||||
self.authorization
|
||||
.require(
|
||||
Subject::User(caller_id),
|
||||
Permission::Read,
|
||||
Resource::File(file_uuid),
|
||||
)
|
||||
.await?;
|
||||
self.storage.get_audio_metadata(&file_uuid).await
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1865,7 +1865,7 @@ impl AppServiceFactory {
|
||||
audio_metadata_repo,
|
||||
),
|
||||
);
|
||||
let music_svc = Arc::new(MusicService::new(music_storage));
|
||||
let music_svc = Arc::new(MusicService::new(music_storage, authorization.clone()));
|
||||
app_state.music_service = Some(music_svc);
|
||||
tracing::info!("Music service initialized");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::application::dtos::display_helpers::{
|
||||
@@ -66,7 +66,8 @@ pub async fn add_favorite(
|
||||
Json(serde_json::json!({
|
||||
"error": "Item type must be 'file' or 'folder'"
|
||||
})),
|
||||
);
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match favorites_service
|
||||
@@ -81,16 +82,14 @@ pub async fn add_favorite(
|
||||
"message": "Item added to favorites"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error adding to favorites: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to add to favorites"
|
||||
})),
|
||||
)
|
||||
}
|
||||
// Route through AppError so the `DomainError::kind` maps to the
|
||||
// right status code (NotFound → 404 anti-enum for the pre-write
|
||||
// authz gate, InvalidInput → 400 for a malformed UUID, etc.).
|
||||
// A hardcoded 500 here would mask the 404 the Round 1 AuthZ
|
||||
// fix relies on.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +128,7 @@ pub async fn remove_favorite(
|
||||
"message": "Item removed from favorites"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
info!("Item {} '{}' was not in favorites", item_type, item_id);
|
||||
(
|
||||
@@ -137,17 +137,12 @@ pub async fn remove_favorite(
|
||||
"message": "Item was not in favorites"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error removing from favorites: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to remove from favorites"
|
||||
})),
|
||||
)
|
||||
}
|
||||
// Same rationale as `add_favorite` — preserve DomainError→HTTP
|
||||
// status mapping instead of collapsing every error to 500.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,15 +342,10 @@ pub async fn batch_add_favorites(
|
||||
);
|
||||
(StatusCode::OK, Json(serde_json::json!(result))).into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error in batch add favorites: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to batch add favorites"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
// Preserve DomainError→HTTP status mapping — the Round 1
|
||||
// AuthZ fix relies on a per-item NotFound propagating out
|
||||
// of the batch. A hardcoded 500 would mask the 404 that
|
||||
// signals a cross-tenant probe.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
|
||||
use crate::application::dtos::display_helpers::{
|
||||
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||
@@ -70,16 +70,10 @@ pub async fn record_item_access(
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error recording access in recents: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to record access"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
// Preserve DomainError→HTTP status mapping — the Round 1
|
||||
// AuthZ fix relies on the NotFound from `authz.require`
|
||||
// propagating as 404 (anti-enum), not being masked as 500.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,16 +124,9 @@ pub async fn remove_from_recent(
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error removing from recents: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to remove from recents"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
// Same rationale as `record_item_access` — preserve the
|
||||
// DomainError→HTTP mapping instead of collapsing to 500.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,16 +157,9 @@ pub async fn clear_recent_items(
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error clearing recent items: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({
|
||||
"error": "Failed to clear recent items"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
// Same rationale as `record_item_access` — preserve the
|
||||
// DomainError→HTTP mapping instead of collapsing to 500.
|
||||
Err(err) => AppError::from(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user