Close data leaks: remove anonymous user creation, hide private playlists, validate uploads and clamp gateway ranges
This commit is contained in:
@@ -43,7 +43,7 @@ public class CreateAlbumCommandHandler(
|
||||
}
|
||||
|
||||
var pictureIntent = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration, request.PictureIntentId, request.UserId, cancellationToken);
|
||||
uploadIntentConfiguration, request.PictureIntentId, request.UserId, UploadIntentPurpose.AlbumPicture, cancellationToken);
|
||||
if (pictureIntent.IsError)
|
||||
return pictureIntent.Errors;
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class UpdateAlbumCommandHandler(
|
||||
{
|
||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
|
||||
request.NewPictureIntentId.Value, request.UserId, UploadIntentPurpose.AlbumPicture, cancellationToken);
|
||||
if (validation.IsError)
|
||||
return validation.Errors;
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ public class CreatePlayListCommandHandler(
|
||||
}
|
||||
|
||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, cancellationToken);
|
||||
uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, UploadIntentPurpose.PlayListPicture, cancellationToken);
|
||||
|
||||
if (validation.IsError)
|
||||
return validation.Errors;
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using X.PagedList;
|
||||
using X.PagedList.EF;
|
||||
|
||||
@@ -12,7 +13,8 @@ namespace Musify.Application.PlayLists;
|
||||
public record GetPlayListTracksQuery(
|
||||
Guid PlayListId,
|
||||
int PageNumber,
|
||||
int PageSize)
|
||||
int PageSize,
|
||||
long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
|
||||
|
||||
public class GetPlayListTracksQueryHandler(IDatabase database)
|
||||
@@ -22,7 +24,10 @@ public class GetPlayListTracksQueryHandler(IDatabase database)
|
||||
{
|
||||
var playListExists = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.AnyAsync(p => p.Id == request.PlayListId, cancellationToken);
|
||||
.AnyAsync(
|
||||
p => p.Id == request.PlayListId
|
||||
&& (p.Visibility == PlaylistVisibility.Public || p.UserId == request.ViewerId),
|
||||
cancellationToken);
|
||||
if (!playListExists)
|
||||
return Error.NotFound();
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Shared;
|
||||
using X.PagedList;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.PlayLists;
|
||||
|
||||
public record GetPlayListsQuery(int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>;
|
||||
|
||||
public class GetPlayListsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetPlayListsQuery, ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>> Handle(GetPlayListsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var totalCount = await database.PlayLists.CountAsync(cancellationToken);
|
||||
|
||||
var pagedEntities = await database.PlayLists
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.Id)
|
||||
.Select(p => new
|
||||
{
|
||||
PlayList = p,
|
||||
CoverTrackIds = p.PlayListTracks
|
||||
.OrderBy(plt => plt.Position)
|
||||
.Take(PlayListApplicationResponse.CoverTrackCount)
|
||||
.Select(plt => plt.TrackId)
|
||||
.ToList()
|
||||
})
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedPlayLists = new StaticPagedList<PlayListApplicationResponse>(
|
||||
pagedEntities.Select(entry => PlayListApplicationResponse.FromEntity(entry.PlayList, entry.CoverTrackIds)),
|
||||
pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount);
|
||||
|
||||
return PaginatedResponse<PlayListApplicationResponse>.FromPagedList(pagedPlayLists);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public record GetPlayListsByUserIdQuery(
|
||||
string? Name,
|
||||
int PageNumber,
|
||||
int PageSize,
|
||||
bool OnlyPublic = false)
|
||||
long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>;
|
||||
|
||||
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||
@@ -25,12 +25,8 @@ public class GetPlayListsByUserIdQueryHandler(IDatabase database)
|
||||
{
|
||||
var playListsQuery = database.PlayLists
|
||||
.AsNoTracking()
|
||||
.Where(p => p.UserId == request.UserId);
|
||||
|
||||
if (request.OnlyPublic)
|
||||
{
|
||||
playListsQuery = playListsQuery.Where(p => p.Visibility == PlaylistVisibility.Public);
|
||||
}
|
||||
.Where(p => p.UserId == request.UserId
|
||||
&& (p.Visibility == PlaylistVisibility.Public || p.UserId == request.ViewerId));
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Name))
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ public class UpdatePlayListCommandHandler(
|
||||
{
|
||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
|
||||
request.NewPictureIntentId.Value, request.UserId, UploadIntentPurpose.PlayListPicture, cancellationToken);
|
||||
if (validation.IsError)
|
||||
return validation.Errors;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ public sealed class UploadIntentValidator(
|
||||
UploadIntentConfiguration config,
|
||||
Guid intentId,
|
||||
long userId,
|
||||
UploadIntentPurpose purpose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var intent = await database.UploadIntents
|
||||
@@ -45,6 +46,9 @@ public sealed class UploadIntentValidator(
|
||||
if (intent == null || intent.UserId != userId)
|
||||
return Error.NotFound(description: "Upload intent not found or not accessible.");
|
||||
|
||||
if (intent.Purpose != purpose)
|
||||
return Error.Validation(description: "Upload intent was issued for a different purpose.");
|
||||
|
||||
if (intent.IsConsumed)
|
||||
return Error.Conflict(description: "Upload intent has already been consumed.");
|
||||
|
||||
|
||||
@@ -59,15 +59,18 @@ public class CreateTrackCommandHandler(
|
||||
return Error.Validation(description: $"Tags '{conflict.First}' and '{conflict.Second}' are not compatible.");
|
||||
}
|
||||
|
||||
if (request.PictureIntentId == request.AudioIntentId)
|
||||
return Error.Validation(description: "Picture and audio must use different upload intents.");
|
||||
|
||||
var pictureValidation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.PictureIntentId, request.UserId, cancellationToken);
|
||||
request.PictureIntentId, request.UserId, UploadIntentPurpose.TrackPicture, cancellationToken);
|
||||
if (pictureValidation.IsError)
|
||||
return pictureValidation.Errors;
|
||||
|
||||
var audioValidation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.AudioIntentId, request.UserId, cancellationToken);
|
||||
request.AudioIntentId, request.UserId, UploadIntentPurpose.TrackAudio, cancellationToken);
|
||||
if (audioValidation.IsError)
|
||||
return audioValidation.Errors;
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Users.Responses;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record CreateUserCommand(long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName)
|
||||
: ICommand<ErrorOr<UserApplicationResponse>>
|
||||
{
|
||||
public static User ToEntity(CreateUserCommand command)
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Id = command.Id,
|
||||
Name = command.Name,
|
||||
NormalizedName = command.Name.ToUpperInvariant(),
|
||||
FirstName = command.FirstName,
|
||||
SecondName = command.SecondName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateUserCommandHandler(IDatabase database, ILogger<CreateUserCommandHandler> logger)
|
||||
: ICommandHandler<CreateUserCommand, ErrorOr<UserApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken);
|
||||
if (userExists)
|
||||
{
|
||||
logger.LogInformation("User {UserId} already exists", request.Id);
|
||||
return Error.Conflict(description: $"User {request.Id} already exists");
|
||||
}
|
||||
|
||||
var newUser = CreateUserCommand.ToEntity(request);
|
||||
|
||||
try
|
||||
{
|
||||
await database.Users.AddAsync(newUser, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to create user {UserId}", request.Id);
|
||||
return Error.Failure(description: $"Failed to create user {request.Id}");
|
||||
}
|
||||
|
||||
logger.LogInformation("Created user {UserId}", newUser.Id);
|
||||
return UserApplicationResponse.FromEntity(newUser);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user