Add access control for media downloads

Introduced IAccesserToDownloadMedia and its implementation to enforce access checks when downloading media files. Updated MediaController to use the new accesser service and improved error handling and validation in upload/download actions. Refactored and moved MediaService to the Medias namespace, registered new services in DI, and added comprehensive tests for access logic. Also fixed GroupMembershipConfiguration to make InvitationId optional and performed minor test and namespace cleanups.
This commit is contained in:
Artemy
2025-07-21 14:51:57 +07:00
parent 58e7716ded
commit c0d02e0fa1
11 changed files with 243 additions and 30 deletions
+37 -25
View File
@@ -1,9 +1,6 @@
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Contracts.Requests;
using Govor.Core.Models;
using Govor.Core.Repositories.MediasAttachments;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -16,14 +13,17 @@ public class MediaController : Controller
{
private readonly ILogger<MediaController> _logger;
private readonly IMediaService _mediaService;
private readonly IAccesserToDownloadMedia _accesser;
private readonly ICurrentUserService _currentUserService;
public MediaController(
ILogger<MediaController> logger,
IMediaService mediaService,
IAccesserToDownloadMedia accesser,
ICurrentUserService currentUserService)
{
_logger = logger;
_accesser = accesser;
_mediaService = mediaService;
_currentUserService = currentUserService;
}
@@ -32,20 +32,21 @@ public class MediaController : Controller
[RequestSizeLimit(20_000_000)] // ~20MB
public async Task<IActionResult> Upload([FromForm] MediaUploadRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (request?.FromFile is null || request.FromFile.Length == 0)
return BadRequest("No file uploaded");
if (request.FromFile.Length > 20_000_000)
return BadRequest("File is too large");
if (string.IsNullOrWhiteSpace(request.MimeType))
return BadRequest("Missing MIME type");
try
{
if (request.FromFile.Length > 20_000_000)
return BadRequest("File is too large");
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Чтение байт из IFormFile
using var memoryStream = new MemoryStream();
await request.FromFile.CopyToAsync(memoryStream);
byte[] fileBytes = memoryStream.ToArray();
byte[] fileBytes = await ReadFileAsync(request.FromFile);
var media = new Media(
_currentUserService.GetCurrentUserId(),
@@ -59,21 +60,21 @@ public class MediaController : Controller
var result = await _mediaService.UploadMediaAsync(media);
_logger.LogInformation(
$"Uploaded file: {Path.GetFileName(request.FromFile.FileName)} from user {_currentUserService.GetCurrentUserId()}");
_logger.LogInformation("Uploaded file {FileName} from user {UserId}",
media.FileName, media.UploaderId);
return Ok(result);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Unauthorized(ex.Message);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error uploading media");
@@ -81,21 +82,32 @@ public class MediaController : Controller
}
}
private async Task<byte[]> ReadFileAsync(IFormFile file)
{
await using var ms = new MemoryStream();
await file.CopyToAsync(ms);
return ms.ToArray();
}
[HttpGet("download/{id}")]
public async Task<IActionResult> Download(Guid id)
{
try
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var userId = _currentUserService.GetCurrentUserId();
if (!await _accesser.HasAccessAsync(id, userId))
return Forbid();
var media = await _mediaService.GetMediaByIdAsync(id);
return File(media.Data, media.MineType, Path.GetFileName(media.FileName));
}
catch (KeyNotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(ex.Message);
return NotFound("Media not found");
}
catch (Exception ex)
{
@@ -11,6 +11,7 @@ using Govor.Application.Interfaces.UserSession;
using Govor.Application.Services;
using Govor.Application.Services.Authentication;
using Govor.Application.Services.Friends;
using Govor.Application.Services.Medias;
using Govor.Application.Services.Messages;
using Govor.Application.Services.UserSessions;
using Govor.Core.Infrastructure.Extensions;
@@ -68,6 +69,7 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IUserGroupsService, UserGroupsService>();
services.AddScoped<IMessagesLoader, MessagesLoader>();
services.AddScoped<IMediaService, MediaService>();
services.AddScoped<IAccesserToDownloadMedia, AccesserToDownloadMediaService>();
// UserSession
services.AddScoped<IUserSessionOpener, UserSessionOpener>();