浏览代码

Add permissions for LiveTV access and management

Shadowghost 2 年之前
父节点
当前提交
eeb0f7af6c

+ 10 - 0
Jellyfin.Api/Constants/Policies.cs

@@ -74,4 +74,14 @@ public static class Policies
     /// Policy name for accessing collection management.
     /// Policy name for accessing collection management.
     /// </summary>
     /// </summary>
     public const string CollectionManagement = "CollectionManagement";
     public const string CollectionManagement = "CollectionManagement";
+
+    /// <summary>
+    /// Policy name for accessing LiveTV.
+    /// </summary>
+    public const string LiveTvAccess = "LiveTvAccess";
+
+    /// <summary>
+    /// Policy name for managing LiveTV.
+    /// </summary>
+    public const string LiveTvManagement = "LiveTvManagement";
 }
 }

+ 40 - 72
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -10,20 +10,19 @@ using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -94,7 +93,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Info")]
     [HttpGet("Info")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<LiveTvInfo> GetLiveTvInfo()
     public ActionResult<LiveTvInfo> GetLiveTvInfo()
     {
     {
         return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
         return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
@@ -130,7 +129,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Channels")]
     [HttpGet("Channels")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
     public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
         [FromQuery] ChannelType? type,
         [FromQuery] ChannelType? type,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -209,7 +208,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
     /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
     [HttpGet("Channels/{channelId}")]
     [HttpGet("Channels/{channelId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
     public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
     {
     {
         var user = userId is null || userId.Value.Equals(default)
         var user = userId is null || userId.Value.Equals(default)
@@ -250,7 +249,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
     /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
     [HttpGet("Recordings")]
     [HttpGet("Recordings")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
     public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
         [FromQuery] string? channelId,
         [FromQuery] string? channelId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -321,7 +320,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
     /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
     [HttpGet("Recordings/Series")]
     [HttpGet("Recordings/Series")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [Obsolete("This endpoint is obsolete.")]
     [Obsolete("This endpoint is obsolete.")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
@@ -364,7 +363,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
     /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
     [HttpGet("Recordings/Groups")]
     [HttpGet("Recordings/Groups")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [Obsolete("This endpoint is obsolete.")]
     [Obsolete("This endpoint is obsolete.")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
     public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
     public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
@@ -380,7 +379,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
     /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
     [HttpGet("Recordings/Folders")]
     [HttpGet("Recordings/Folders")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
     public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
     {
     {
         var user = userId is null || userId.Value.Equals(default)
         var user = userId is null || userId.Value.Equals(default)
@@ -402,7 +401,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
     /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
     [HttpGet("Recordings/{recordingId}")]
     [HttpGet("Recordings/{recordingId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
     public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
     {
     {
         var user = userId is null || userId.Value.Equals(default)
         var user = userId is null || userId.Value.Equals(default)
@@ -424,10 +423,9 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("Tuners/{tunerId}/Reset")]
     [HttpPost("Tuners/{tunerId}/Reset")]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
     public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
         await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -442,7 +440,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Timers/{timerId}")]
     [HttpGet("Timers/{timerId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
     public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
     {
     {
         return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
         return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
@@ -458,7 +456,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Timers/Defaults")]
     [HttpGet("Timers/Defaults")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
     public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
     {
     {
         return string.IsNullOrEmpty(programId)
         return string.IsNullOrEmpty(programId)
@@ -478,7 +476,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Timers")]
     [HttpGet("Timers")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
     public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
         [FromQuery] string? channelId,
         [FromQuery] string? channelId,
         [FromQuery] string? seriesTimerId,
         [FromQuery] string? seriesTimerId,
@@ -532,7 +530,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpGet("Programs")]
     [HttpGet("Programs")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -615,7 +613,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// </returns>
     /// </returns>
     [HttpPost("Programs")]
     [HttpPost("Programs")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
     {
     {
         var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
         var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
@@ -681,7 +679,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Recommended epgs returned.</response>
     /// <response code="200">Recommended epgs returned.</response>
     /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
     /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
     [HttpGet("Programs/Recommended")]
     [HttpGet("Programs/Recommended")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -733,7 +731,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Program returned.</response>
     /// <response code="200">Program returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
     /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
     [HttpGet("Programs/{programId}")]
     [HttpGet("Programs/{programId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<BaseItemDto>> GetProgram(
     public async Task<ActionResult<BaseItemDto>> GetProgram(
         [FromRoute, Required] string programId,
         [FromRoute, Required] string programId,
@@ -754,13 +752,11 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="404">Item not found.</response>
     /// <response code="404">Item not found.</response>
     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
     [HttpDelete("Recordings/{recordingId}")]
     [HttpDelete("Recordings/{recordingId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
+    public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
-
         var item = _libraryManager.GetItemById(recordingId);
         var item = _libraryManager.GetItemById(recordingId);
         if (item is null)
         if (item is null)
         {
         {
@@ -782,11 +778,10 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Timer deleted.</response>
     /// <response code="204">Timer deleted.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete("Timers/{timerId}")]
     [HttpDelete("Timers/{timerId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
     public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
         await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -799,12 +794,11 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Timer updated.</response>
     /// <response code="204">Timer updated.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("Timers/{timerId}")]
     [HttpPost("Timers/{timerId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
     public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
     public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
         await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -816,11 +810,10 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Timer created.</response>
     /// <response code="204">Timer created.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("Timers")]
     [HttpPost("Timers")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
     public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
         await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -833,7 +826,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="404">Series timer not found.</response>
     /// <response code="404">Series timer not found.</response>
     /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
     /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
     [HttpGet("SeriesTimers/{timerId}")]
     [HttpGet("SeriesTimers/{timerId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
     public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
@@ -855,7 +848,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Timers returned.</response>
     /// <response code="200">Timers returned.</response>
     /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
     /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
     [HttpGet("SeriesTimers")]
     [HttpGet("SeriesTimers")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
     public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
     {
     {
@@ -875,11 +868,10 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Timer cancelled.</response>
     /// <response code="204">Timer cancelled.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete("SeriesTimers/{timerId}")]
     [HttpDelete("SeriesTimers/{timerId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
     public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
         await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -892,12 +884,11 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Series timer updated.</response>
     /// <response code="204">Series timer updated.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("SeriesTimers/{timerId}")]
     [HttpPost("SeriesTimers/{timerId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
     public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
     public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
         await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -909,11 +900,10 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Series timer info created.</response>
     /// <response code="204">Series timer info created.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("SeriesTimers")]
     [HttpPost("SeriesTimers")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
     public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
     {
     {
-        await AssertUserCanManageLiveTv().ConfigureAwait(false);
         await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
         await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
@@ -924,7 +914,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="groupId">Group id.</param>
     /// <param name="groupId">Group id.</param>
     /// <returns>A <see cref="NotFoundResult"/>.</returns>
     /// <returns>A <see cref="NotFoundResult"/>.</returns>
     [HttpGet("Recordings/Groups/{groupId}")]
     [HttpGet("Recordings/Groups/{groupId}")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("This endpoint is obsolete.")]
     [Obsolete("This endpoint is obsolete.")]
     public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
     public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
@@ -938,7 +928,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Guid info returned.</response>
     /// <response code="200">Guid info returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
     /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
     [HttpGet("GuideInfo")]
     [HttpGet("GuideInfo")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<GuideInfo> GetGuideInfo()
     public ActionResult<GuideInfo> GetGuideInfo()
     {
     {
@@ -952,7 +942,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Created tuner host returned.</response>
     /// <response code="200">Created tuner host returned.</response>
     /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
     /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
     [HttpPost("TunerHosts")]
     [HttpPost("TunerHosts")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
     public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
     {
     {
@@ -966,7 +956,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Tuner host deleted.</response>
     /// <response code="204">Tuner host deleted.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete("TunerHosts")]
     [HttpDelete("TunerHosts")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public ActionResult DeleteTunerHost([FromQuery] string? id)
     public ActionResult DeleteTunerHost([FromQuery] string? id)
     {
     {
@@ -982,7 +972,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Default listings provider info returned.</response>
     /// <response code="200">Default listings provider info returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
     /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
     [HttpGet("ListingProviders/Default")]
     [HttpGet("ListingProviders/Default")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
     public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
     {
     {
@@ -999,7 +989,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Created listings provider returned.</response>
     /// <response code="200">Created listings provider returned.</response>
     /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
     /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
     [HttpPost("ListingProviders")]
     [HttpPost("ListingProviders")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
     public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
     public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@@ -1025,7 +1015,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="204">Listing provider deleted.</response>
     /// <response code="204">Listing provider deleted.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete("ListingProviders")]
     [HttpDelete("ListingProviders")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     public ActionResult DeleteListingProvider([FromQuery] string? id)
     public ActionResult DeleteListingProvider([FromQuery] string? id)
     {
     {
@@ -1043,7 +1033,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Available lineups returned.</response>
     /// <response code="200">Available lineups returned.</response>
     /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
     /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
     [HttpGet("ListingProviders/Lineups")]
     [HttpGet("ListingProviders/Lineups")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
     public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
         [FromQuery] string? id,
         [FromQuery] string? id,
@@ -1060,7 +1050,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Available countries returned.</response>
     /// <response code="200">Available countries returned.</response>
     /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
     /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
     [HttpGet("ListingProviders/SchedulesDirect/Countries")]
     [HttpGet("ListingProviders/SchedulesDirect/Countries")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesFile(MediaTypeNames.Application.Json)]
     [ProducesFile(MediaTypeNames.Application.Json)]
     public async Task<ActionResult> GetSchedulesDirectCountries()
     public async Task<ActionResult> GetSchedulesDirectCountries()
@@ -1081,7 +1071,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Channel mapping options returned.</response>
     /// <response code="200">Channel mapping options returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
     /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
     [HttpGet("ChannelMappingOptions")]
     [HttpGet("ChannelMappingOptions")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
     public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
     {
     {
@@ -1119,7 +1109,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Created channel mapping returned.</response>
     /// <response code="200">Created channel mapping returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
     /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
     [HttpPost("ChannelMappings")]
     [HttpPost("ChannelMappings")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
     public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
     {
     {
@@ -1132,7 +1122,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <response code="200">Tuner host types returned.</response>
     /// <response code="200">Tuner host types returned.</response>
     /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
     /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
     [HttpGet("TunerHosts/Types")]
     [HttpGet("TunerHosts/Types")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
     public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
     {
     {
@@ -1147,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
     /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
     [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
     [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
     [HttpGet("Tuners/Discover")]
     [HttpGet("Tuners/Discover")]
-    [Authorize]
+    [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
     public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
     {
     {
@@ -1207,26 +1197,4 @@ public class LiveTvController : BaseJellyfinApiController
         var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
         var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
         return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
         return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
     }
     }
-
-    private async Task AssertUserCanManageLiveTv()
-    {
-        var user = _userManager.GetUserById(User.GetUserId()) ?? throw new ResourceNotFoundException();
-        var session = await _sessionManager.LogSessionActivity(
-            User.GetClient(),
-            User.GetVersion(),
-            User.GetDeviceId(),
-            User.GetDevice(),
-            HttpContext.GetNormalizedRemoteIp().ToString(),
-            user).ConfigureAwait(false);
-
-        if (session.UserId.Equals(default))
-        {
-            throw new SecurityException("Anonymous live tv management is not allowed.");
-        }
-
-        if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
-        {
-            throw new SecurityException("The current user does not have permission to manage live tv.");
-        }
-    }
 }
 }

+ 2 - 1
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -75,7 +75,8 @@ namespace Jellyfin.Server.Extensions
                 options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
                 options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
                 options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
                 options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
                 options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement));
                 options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement));
-                options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
+                options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess));
+                options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement));
                 options.AddPolicy(
                 options.AddPolicy(
                     Policies.RequiresElevation,
                     Policies.RequiresElevation,
                     policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
                     policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)