Bladeren bron

Secure endpoints in LiveTvController

Shadowghost 2 jaren geleden
bovenliggende
commit
885a1b02c1

+ 19 - 1
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs

@@ -43,12 +43,16 @@ namespace Jellyfin.Api.Auth
         /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
         /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
         /// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
+        /// <param name="requireLiveTvManagementPermission">Whether validation requires LiveTV management permission.</param>
+        /// <param name="requireLiveTvAccessPermission">Whether validation requires LiveTV management permission.</param>
         /// <returns>Validated claim status.</returns>
         protected bool ValidateClaims(
             ClaimsPrincipal claimsPrincipal,
             bool ignoreSchedule = false,
             bool localAccessOnly = false,
-            bool requiredDownloadPermission = false)
+            bool requiredDownloadPermission = false,
+            bool requireLiveTvManagementPermission = false,
+            bool requireLiveTvAccessPermission = false)
         {
             // ApiKey is currently global admin, always allow.
             var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
@@ -106,6 +110,20 @@ namespace Jellyfin.Api.Auth
                 return false;
             }
 
+            // User attempting to access LiveTV without permission.
+            if (requireLiveTvAccessPermission
+                && !user.HasPermission(PermissionKind.EnableLiveTvAccess))
+            {
+                return false;
+            }
+
+            // User attempting to manage LiveTV without permission.
+            if (requireLiveTvManagementPermission
+                && !user.HasPermission(PermissionKind.EnableLiveTvManagement))
+            {
+                return false;
+            }
+
             return true;
         }
     }

+ 43 - 0
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessHandler.cs

@@ -0,0 +1,43 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
+
+/// <summary>
+/// Authorization handler for LiveTV access.
+/// </summary>
+public class LiveTvAccessHandler : BaseAuthorizationHandler<LiveTvAccessRequirement>
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="LiveTvAccessHandler"/> class.
+    /// </summary>
+    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+    /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+    public LiveTvAccessHandler(
+        IUserManager userManager,
+        INetworkManager networkManager,
+        IHttpContextAccessor httpContextAccessor)
+        : base(userManager, networkManager, httpContextAccessor)
+    {
+    }
+
+    /// <inheritdoc />
+    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvAccessRequirement requirement)
+    {
+        var validated = ValidateClaims(context.User, requireLiveTvAccessPermission: true);
+        if (validated)
+        {
+            context.Succeed(requirement);
+        }
+        else
+        {
+            context.Fail();
+        }
+
+        return Task.CompletedTask;
+    }
+}

+ 10 - 0
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessRequirement.cs

@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
+
+/// <summary>
+/// The LiveTV access requirement.
+/// </summary>
+public class LiveTvAccessRequirement : IAuthorizationRequirement
+{
+}

+ 43 - 0
Jellyfin.Api/Auth/LiveTvManagementPolicy/LiveTvManagementHandler.cs

@@ -0,0 +1,43 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
+
+/// <summary>
+/// Authorization handler for LiveTV management access.
+/// </summary>
+public class LiveTvManagementHandler : BaseAuthorizationHandler<LiveTvManagementRequirement>
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="LiveTvManagementHandler"/> class.
+    /// </summary>
+    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+    /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+    public LiveTvManagementHandler(
+        IUserManager userManager,
+        INetworkManager networkManager,
+        IHttpContextAccessor httpContextAccessor)
+        : base(userManager, networkManager, httpContextAccessor)
+    {
+    }
+
+    /// <inheritdoc />
+    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvManagementRequirement requirement)
+    {
+        var validated = ValidateClaims(context.User, requireLiveTvManagementPermission: true);
+        if (validated)
+        {
+            context.Succeed(requirement);
+        }
+        else
+        {
+            context.Fail();
+        }
+
+        return Task.CompletedTask;
+    }
+}

+ 10 - 0
Jellyfin.Api/Auth/LiveTvManagementPolicy/LiveTvManagementRequirement.cs

@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
+
+/// <summary>
+/// The LiveTV management requirement.
+/// </summary>
+public class LiveTvManagementRequirement : IAuthorizationRequirement
+{
+}

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

@@ -74,5 +74,15 @@ namespace Jellyfin.Api.Constants
         /// Policy name for accessing a SyncPlay group.
         /// </summary>
         public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
+
+        /// <summary>
+        /// Policy name for accessing LiveTV.
+        /// </summary>
+        public const string LiveTvAccess = "LiveTvAccess";
+
+        /// <summary>
+        /// Policy name for managing LiveTV.
+        /// </summary>
+        public const string LiveTvManagement = "LiveTvManagement";
     }
 }

+ 41 - 63
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Info")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<LiveTvInfo> GetLiveTvInfo()
         {
             return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Channels")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
             [FromQuery] ChannelType? type,
             [FromQuery] Guid? userId,
@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
         [HttpGet("Channels/{channelId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
         {
             var user = userId is null || userId.Value.Equals(default)
@@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
         [HttpGet("Recordings")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
             [FromQuery] string? channelId,
             [FromQuery] Guid? userId,
@@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
         [HttpGet("Recordings/Series")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [Obsolete("This endpoint is obsolete.")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
@@ -363,7 +363,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
         [HttpGet("Recordings/Groups")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [Obsolete("This endpoint is obsolete.")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
         public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
@@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
         [HttpGet("Recordings/Folders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
         {
             var user = userId is null || userId.Value.Equals(default)
@@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
         [HttpGet("Recordings/{recordingId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
         {
             var user = userId is null || userId.Value.Equals(default)
@@ -423,10 +423,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Tuners/{tunerId}/Reset")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -441,7 +440,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Timers/{timerId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
         {
             return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
@@ -457,7 +456,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Timers/Defaults")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
         {
             return string.IsNullOrEmpty(programId)
@@ -477,7 +476,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Timers")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
             [FromQuery] string? channelId,
             [FromQuery] string? seriesTimerId,
@@ -531,7 +530,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpGet("Programs")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
             [FromQuery] Guid? userId,
@@ -614,7 +613,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpPost("Programs")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
         {
             var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
@@ -680,7 +679,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Recommended epgs returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
         [HttpGet("Programs/Recommended")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
             [FromQuery] Guid? userId,
@@ -732,7 +731,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Program returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
         [HttpGet("Programs/{programId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<BaseItemDto>> GetProgram(
             [FromRoute, Required] string programId,
@@ -753,13 +752,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpDelete("Recordings/{recordingId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [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);
             if (item == null)
             {
@@ -781,11 +778,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Timer deleted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("Timers/{timerId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
             return NoContent();
         }
@@ -798,12 +794,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Timer updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Timers/{timerId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -815,11 +810,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Timer created.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Timers")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -832,7 +826,7 @@ namespace Jellyfin.Api.Controllers
         /// <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>
         [HttpGet("SeriesTimers/{timerId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
@@ -854,7 +848,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Timers returned.</response>
         /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
         [HttpGet("SeriesTimers")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
         {
@@ -874,11 +868,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Timer cancelled.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("SeriesTimers/{timerId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
             return NoContent();
         }
@@ -891,12 +884,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Series timer updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("SeriesTimers/{timerId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -908,11 +900,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Series timer info created.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("SeriesTimers")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
         {
-            await AssertUserCanManageLiveTv().ConfigureAwait(false);
             await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
             return NoContent();
         }
@@ -923,7 +914,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="groupId">Group id.</param>
         /// <returns>A <see cref="NotFoundResult"/>.</returns>
         [HttpGet("Recordings/Groups/{groupId}")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [Obsolete("This endpoint is obsolete.")]
         public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
@@ -937,7 +928,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Guid info returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
         [HttpGet("GuideInfo")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<GuideInfo> GetGuideInfo()
         {
@@ -951,7 +942,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Created tuner host returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
         [HttpPost("TunerHosts")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
         {
@@ -965,7 +956,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Tuner host deleted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("TunerHosts")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DeleteTunerHost([FromQuery] string? id)
         {
@@ -981,7 +972,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Default listings provider info returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
         [HttpGet("ListingProviders/Default")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
         {
@@ -998,7 +989,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Created listings provider returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
         [HttpPost("ListingProviders")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@@ -1025,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Listing provider deleted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("ListingProviders")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DeleteListingProvider([FromQuery] string? id)
         {
@@ -1043,7 +1034,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Available lineups returned.</response>
         /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
         [HttpGet("ListingProviders/Lineups")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
             [FromQuery] string? id,
@@ -1060,7 +1051,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Available countries returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
         [HttpGet("ListingProviders/SchedulesDirect/Countries")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesFile(MediaTypeNames.Application.Json)]
         public async Task<ActionResult> GetSchedulesDirectCountries()
@@ -1081,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Channel mapping options returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
         [HttpGet("ChannelMappingOptions")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
         {
@@ -1119,7 +1110,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Created channel mapping returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
         [HttpPost("ChannelMappings")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
         {
@@ -1132,7 +1123,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Tuner host types returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
         [HttpGet("TunerHosts/Types")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
         {
@@ -1147,7 +1138,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
         [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
         [HttpGet("Tuners/Discover")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.LiveTvManagement)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
         {
@@ -1165,6 +1156,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if recording not found.
         /// </returns>
         [HttpGet("LiveRecordings/{recordingId}/stream")]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesVideoFile]
@@ -1193,6 +1185,7 @@ namespace Jellyfin.Api.Controllers
         /// or a <see cref="NotFoundResult"/> if stream not found.
         /// </returns>
         [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
+        [Authorize(Policy = Policies.LiveTvAccess)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesVideoFile]
@@ -1207,20 +1200,5 @@ namespace Jellyfin.Api.Controllers
             var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
             return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
         }
-
-        private async Task AssertUserCanManageLiveTv()
-        {
-            var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
-
-            if (user == null)
-            {
-                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.");
-            }
-        }
     }
 }

+ 18 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -14,6 +14,8 @@ using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
 using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.LiveTvAccessPolicy;
+using Jellyfin.Api.Auth.LiveTvManagementPolicy;
 using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
@@ -66,6 +68,8 @@ namespace Jellyfin.Server.Extensions
             serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvAccessHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvManagementHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
             return serviceCollection.AddAuthorizationCore(options =>
             {
@@ -167,6 +171,20 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new AnonymousLanAccessRequirement());
                     });
+                options.AddPolicy(
+                    Policies.LiveTvAccess,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LiveTvAccessRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.LiveTvManagement,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LiveTvManagementRequirement());
+                    });
             });
         }