{"record":{"id":"15de8911a300ee90","repo":"Kareadita/Kavita","slug":"user-no-access-library-from-series","errorCode":"user-no-access-library-from-series","errorMessage":"user-no-access-library-from-series","messagePattern":"user-no-access-library-from-series","errorType":"exception","errorClass":"UnauthorizedAccessException","httpStatus":401,"severity":"error","filePath":"Kavita.Services/SeriesService.cs","lineNumber":532,"sourceCode":"            return false;\n        }\n    }\n\n    /// <summary>\n    /// This generates all the arrays needed by the Series Detail page in the UI. It is a specialized API for the unique layout constraints.\n    /// </summary>\n    /// <param name=\"seriesId\"></param>\n    /// <param name=\"userId\"></param>\n    /// <param name=\"ct\"></param>\n    /// <returns></returns>\n    public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default)\n    {\n        var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\n        if (series == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"series-doesnt-exist\"));\n\n        var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);\n        if (!libraryIds.Contains(series.LibraryId))\n            throw new UnauthorizedAccessException(\"user-no-access-library-from-series\");\n\n        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct);\n        if (user!.AgeRestriction != AgeRating.NotApplicable)\n        {\n            var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);\n            if (seriesMetadata!.AgeRating > user.AgeRestriction)\n                throw new UnauthorizedAccessException(\"series-restricted-age-restriction\");\n        }\n\n\n        var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);\n        var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct);\n        var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType);\n        var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;\n\n        // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.\n        var processedVolumes = new List<VolumeDto>();\n        foreach (var volume in volumes)","sourceCodeStart":514,"sourceCodeEnd":550,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/SeriesService.cs#L514-L550","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId);\n\n// after\nvar libs = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);\nvar series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\nif (series is null || !libs.Contains(series.LibraryId))\n    return Forbid();\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);","handlingStrategy":"validation","validationCode":"var libs = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);\nvar series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\nif (series is null || !libs.Contains(series.LibraryId)) return Forbid();\nvar detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);","typeGuard":"static bool UserCanAccessLibrary(int libraryId, IEnumerable<int> userLibs) => userLibs.Contains(libraryId);","tryCatchPattern":"try { return await seriesService.GetSeriesDetail(seriesId, userId, ct); }\ncatch (UnauthorizedAccessException) { return Forbid(); }","preventionTips":["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."],"tags":["series","authorization","access-control","library"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}