Kareadita/Kavita · error · UnauthorizedAccessException
user-no-access-library-from-series
user-no-access-library-from-series
Error message
user-no-access-library-from-series
What it means
Thrown as UnauthorizedAccessException in GetSeriesDetail when the series' LibraryId is not in the set of libraries the user has access to (GetLibraryIdsForUserIdAsync). It is an authorization check: even if the series row exists, the user must belong to a library that contains it.
Source
Thrown at Kavita.Services/SeriesService.cs:532
return false;
}
}
/// <summary>
/// This generates all the arrays needed by the Series Detail page in the UI. It is a specialized API for the unique layout constraints.
/// </summary>
/// <param name="seriesId"></param>
/// <param name="userId"></param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default)
{
var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);
if (series == null) throw new KavitaException(await localizationService.TranslateAsync(userId, "series-doesnt-exist"));
var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);
if (!libraryIds.Contains(series.LibraryId))
throw new UnauthorizedAccessException("user-no-access-library-from-series");
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct);
if (user!.AgeRestriction != AgeRating.NotApplicable)
{
var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);
if (seriesMetadata!.AgeRating > user.AgeRestriction)
throw new UnauthorizedAccessException("series-restricted-age-restriction");
}
var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);
var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct);
var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType);
var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
var processedVolumes = new List<VolumeDto>();
foreach (var volume in volumes)View on GitHub (pinned to 9c3e540000)
Solutions
- Grant the user access to the library containing the series (admin -> libraries -> members).
- Return 403 to the client and hide/redirect away from the inaccessible series.
- Filter series lists by the user's accessible libraries so such ids never surface.
- Audit library membership if access was expected.
Example fix
// before
var detail = await seriesService.GetSeriesDetail(seriesId, userId);
// after
var libs = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);
var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);
if (series is null || !libs.Contains(series.LibraryId))
return Forbid();
var detail = await seriesService.GetSeriesDetail(seriesId, userId, ct); Defensive patterns
Strategy: validation
Validate before calling
var libs = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series is null || !libs.Contains(series.LibraryId)) return Forbid(); var detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);
Type guard
static bool UserCanAccessLibrary(int libraryId, IEnumerable<int> userLibs) => userLibs.Contains(libraryId);
Try / catch
try { return await seriesService.GetSeriesDetail(seriesId, userId, ct); }
catch (UnauthorizedAccessException) { return Forbid(); } Prevention
- Filter series lists by the user's accessible libraries.
- Grant library membership via admin before expecting access.
- Return 403 and hide inaccessible series from the UI.
When it happens
Trigger: GetSeriesDetail called for a series whose LibraryId is absent from the user's accessible library ids - user lacks the library role, or the series was moved to a restricted library.
Common situations: User's library access was revoked; admin moved the series to a library the user can't see; shared link opened by a user without that library; or age/library restriction policy excluded the library.
Related errors
- no-library-access
- series-restricted-age-restriction
- series-restricted-age-restriction
- series-doesnt-exist
- delete-library-while-scan
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/15de8911a300ee90.
Report an issue: GitHub.