Validate Download Claim (#971)

* Partially complete, got some code to validate your Role. Needs to be applied to all methods and made a filter.

* Cleaned up the code on the backend to validate each call. The reason the RequireDownloadRole doesn't work is that the user still has the claim in their token so the simple validation isn't working. We need explicit checks.

* Don't allow users to download files if they have lost the claim but not refreshed token.

* Don't allow users to download files if they have lost the claim but not refreshed token.
This commit is contained in:
Joseph Milazzo 2022-01-20 07:46:59 -08:00 committed by GitHub
parent 7b9ac2faee
commit eb7e2781c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 35 additions and 21 deletions

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.Constants;
using API.Data;
using API.DTOs.Downloads;
using API.Entities;
@ -13,33 +14,32 @@ using API.Services;
using API.SignalR;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace API.Controllers
{
[Authorize(Policy = "RequireDownloadRole")]
[Authorize(Policy="RequireDownloadRole")]
public class DownloadController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IArchiveService _archiveService;
private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService;
private readonly IDownloadService _downloadService;
private readonly IHubContext<MessageHub> _messageHub;
private readonly NumericComparer _numericComparer;
private readonly UserManager<AppUser> _userManager;
private const string DefaultContentType = "application/octet-stream";
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
ICacheService cacheService, IDownloadService downloadService, IHubContext<MessageHub> messageHub)
IDownloadService downloadService, IHubContext<MessageHub> messageHub, UserManager<AppUser> userManager)
{
_unitOfWork = unitOfWork;
_archiveService = archiveService;
_directoryService = directoryService;
_cacheService = cacheService;
_downloadService = downloadService;
_messageHub = messageHub;
_numericComparer = new NumericComparer();
_userManager = userManager;
}
[HttpGet("volume-size")]
@ -63,9 +63,12 @@ namespace API.Controllers
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
[Authorize(Policy="RequireDownloadRole")]
[HttpGet("volume")]
public async Task<ActionResult> DownloadVolume(int volumeId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
@ -79,6 +82,13 @@ namespace API.Controllers
}
}
private async Task<bool> HasDownloadPermission()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var roles = await _userManager.GetRolesAsync(user);
return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole);
}
private async Task<ActionResult> GetFirstFileDownload(IEnumerable<MangaFile> files)
{
var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
@ -88,6 +98,7 @@ namespace API.Controllers
[HttpGet("chapter")]
public async Task<ActionResult> DownloadChapter(int chapterId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
@ -120,6 +131,7 @@ namespace API.Controllers
[HttpGet("series")]
public async Task<ActionResult> DownloadSeries(int seriesId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
try
@ -135,6 +147,8 @@ namespace API.Controllers
[HttpPost("bookmarks")]
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
// We know that all bookmarks will be for one single seriesId
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);

View File

@ -103,17 +103,6 @@ namespace API.Data.Metadata
info.Characters = Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Parser.Parser.CleanAuthor(info.CoverArtist);
// if (!string.IsNullOrEmpty(info.Web))
// {
// // ComicVine stores the Issue number in Number field and does not use Volume.
// if (!info.Web.Contains("https://comicvine.gamespot.com/")) return;
// if (info.Volume.Equals("1"))
// {
// info.Volume = Parser.Parser.DefaultVolume;
// }
// }
}

View File

@ -4,6 +4,7 @@ using API.Constants;
using API.Data;
using API.Entities;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

View File

@ -83,6 +83,10 @@ export class ErrorInterceptor implements HttpInterceptor {
} else {
console.error('error:', error);
if (error.statusText === 'Bad Request') {
if (error.error instanceof Blob) {
this.toastr.error('There was an issue downloading this file or you do not have permissions', error.status);
return;
}
this.toastr.error(error.error, error.status);
} else {
this.toastr.error(error.statusText === 'OK' ? error.error : error.statusText, error.status);
@ -101,7 +105,13 @@ export class ErrorInterceptor implements HttpInterceptor {
console.log('500 error: ', error);
}
this.toastr.error(err.message);
} else {
} else if (error.hasOwnProperty('message') && error.message.trim() !== '') {
if (error.message != 'User is not authenticated') {
console.log('500 error: ', error);
}
this.toastr.error(error.message);
}
else {
this.toastr.error('There was an unknown critical error.');
console.error('500 error:', error);
}

View File

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { Series } from 'src/app/_models/series';
import { environment } from 'src/environments/environment';
@ -10,7 +10,7 @@ import { asyncScheduler, Observable } from 'rxjs';
import { SAVER, Saver } from '../_providers/saver.provider';
import { download, Download } from '../_models/download';
import { PageBookmark } from 'src/app/_models/page-bookmark';
import { throttleTime } from 'rxjs/operators';
import { catchError, throttleTime } from 'rxjs/operators';
const DEBOUNCE_TIME = 100;