Răsfoiți Sursa

Apply fixes from review

crobibero 4 ani în urmă
părinte
comite
fffa94fc33
44 a modificat fișierele cu 386 adăugiri și 134 ștergeri
  1. 56 0
      Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
  2. 11 0
      Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
  3. 51 0
      Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs
  4. 11 0
      Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs
  5. 5 5
      Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
  6. 2 2
      Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
  7. 46 0
      Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
  8. 11 0
      Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
  9. 17 2
      Jellyfin.Api/Constants/Policies.cs
  10. 1 1
      Jellyfin.Api/Controllers/ApiKeyController.cs
  11. 3 2
      Jellyfin.Api/Controllers/CollectionController.cs
  12. 3 3
      Jellyfin.Api/Controllers/ConfigurationController.cs
  13. 7 7
      Jellyfin.Api/Controllers/DevicesController.cs
  14. 3 4
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  15. 4 4
      Jellyfin.Api/Controllers/EnvironmentController.cs
  16. 6 5
      Jellyfin.Api/Controllers/ImageByNameController.cs
  17. 2 0
      Jellyfin.Api/Controllers/ImageController.cs
  18. 2 1
      Jellyfin.Api/Controllers/InstantMixController.cs
  19. 10 11
      Jellyfin.Api/Controllers/ItemLookupController.cs
  20. 3 3
      Jellyfin.Api/Controllers/ItemUpdateController.cs
  21. 5 4
      Jellyfin.Api/Controllers/LibraryController.cs
  22. 2 2
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  23. 1 1
      Jellyfin.Api/Controllers/LocalizationController.cs
  24. 3 2
      Jellyfin.Api/Controllers/MediaInfoController.cs
  25. 9 5
      Jellyfin.Api/Controllers/NotificationsController.cs
  26. 0 1
      Jellyfin.Api/Controllers/PackageController.cs
  27. 3 0
      Jellyfin.Api/Controllers/PersonsController.cs
  28. 2 2
      Jellyfin.Api/Controllers/PlaylistsController.cs
  29. 2 2
      Jellyfin.Api/Controllers/PluginsController.cs
  30. 3 3
      Jellyfin.Api/Controllers/RemoteImageController.cs
  31. 5 5
      Jellyfin.Api/Controllers/ScheduledTasksController.cs
  32. 32 17
      Jellyfin.Api/Controllers/SessionController.cs
  33. 3 3
      Jellyfin.Api/Controllers/SubtitleController.cs
  34. 1 1
      Jellyfin.Api/Controllers/SyncPlayController.cs
  35. 2 5
      Jellyfin.Api/Controllers/SystemController.cs
  36. 2 2
      Jellyfin.Api/Controllers/TimeSyncController.cs
  37. 7 6
      Jellyfin.Api/Controllers/TvShowsController.cs
  38. 5 6
      Jellyfin.Api/Controllers/UserController.cs
  39. 4 6
      Jellyfin.Api/Controllers/VideoAttachmentsController.cs
  40. 2 2
      Jellyfin.Api/Controllers/VideosController.cs
  41. 3 0
      Jellyfin.Api/Controllers/YearsController.cs
  42. 1 1
      Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
  43. 31 4
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  44. 4 4
      tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs

+ 56 - 0
Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs

@@ -0,0 +1,56 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
+{
+    /// <summary>
+    /// Authorization handler for requiring first time setup or elevated privileges.
+    /// </summary>
+    public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
+    {
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
+        /// </summary>
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <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 FirstTimeSetupOrDefaultHandler(
+            IConfigurationManager configurationManager,
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+            _configurationManager = configurationManager;
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement)
+        {
+            if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            {
+                context.Succeed(firstTimeSetupOrElevatedRequirement);
+                return Task.CompletedTask;
+            }
+
+            var validated = ValidateClaims(context.User);
+            if (validated)
+            {
+                context.Succeed(firstTimeSetupOrElevatedRequirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
+{
+    /// <summary>
+    /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
+    /// </summary>
+    public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 51 - 0
Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs

@@ -0,0 +1,51 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
+{
+    /// <summary>
+    /// Escape schedule controls handler.
+    /// </summary>
+    public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
+    {
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IgnoreParentalControlOrFirstTimeSetupHandler"/> 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>
+        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        public IgnoreParentalControlOrFirstTimeSetupHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor,
+            IConfigurationManager configurationManager)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+            _configurationManager = configurationManager;
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, ignoreSchedule: true);
+            if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+            {
+                context.Succeed(requirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy
+{
+    /// <summary>
+    /// Escape schedule controls requirement.
+    /// </summary>
+    public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 5 - 5
Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs → Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs

@@ -4,20 +4,20 @@ using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 
-namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
 {
     /// <summary>
     /// Escape schedule controls handler.
     /// </summary>
-    public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
+    public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
+        /// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> 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 IgnoreScheduleHandler(
+        public IgnoreParentalControlHandler(
             IUserManager userManager,
             INetworkManager networkManager,
             IHttpContextAccessor httpContextAccessor)
@@ -26,7 +26,7 @@ namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
         }
 
         /// <inheritdoc />
-        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
         {
             var validated = ValidateClaims(context.User, ignoreSchedule: true);
             if (!validated)

+ 2 - 2
Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs → Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs

@@ -1,11 +1,11 @@
 using Microsoft.AspNetCore.Authorization;
 
-namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
 {
     /// <summary>
     /// Escape schedule controls requirement.
     /// </summary>
-    public class IgnoreScheduleRequirement : IAuthorizationRequirement
+    public class IgnoreParentalControlRequirement : IAuthorizationRequirement
     {
     }
 }

+ 46 - 0
Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs

@@ -0,0 +1,46 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
+{
+    /// <summary>
+    /// Local access handler.
+    /// </summary>
+    public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> 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 LocalAccessOrRequiresElevationHandler(
+            IUserManager userManager,
+            INetworkManager networkManager,
+            IHttpContextAccessor httpContextAccessor)
+            : base(userManager, networkManager, httpContextAccessor)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
+        {
+            var validated = ValidateClaims(context.User, localAccessOnly: true);
+
+            if (validated || context.User.IsInRole(UserRoles.Administrator))
+            {
+                context.Succeed(requirement);
+            }
+            else
+            {
+                context.Fail();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 11 - 0
Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs

@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
+{
+    /// <summary>
+    /// The local access authorization requirement.
+    /// </summary>
+    public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement
+    {
+    }
+}

+ 17 - 2
Jellyfin.Api/Constants/Policies.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Constants
         /// <summary>
         /// Policy name for requiring first time setup or elevated privileges.
         /// </summary>
-        public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated";
+        public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
 
         /// <summary>
         /// Policy name for requiring elevated privileges.
@@ -28,11 +28,26 @@ namespace Jellyfin.Api.Constants
         /// <summary>
         /// Policy name for escaping schedule controls.
         /// </summary>
-        public const string IgnoreSchedule = "IgnoreSchedule";
+        public const string IgnoreParentalControl = "IgnoreParentalControl";
 
         /// <summary>
         /// Policy name for requiring download permission.
         /// </summary>
         public const string Download = "Download";
+
+        /// <summary>
+        /// Policy name for requiring first time setup or default permissions.
+        /// </summary>
+        public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
+
+        /// <summary>
+        /// Policy name for requiring local access or elevated privileges.
+        /// </summary>
+        public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
+
+        /// <summary>
+        /// Policy name for escaping schedule controls or requiring first time setup.
+        /// </summary>
+        public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup";
     }
 }

+ 1 - 1
Jellyfin.Api/Controllers/ApiKeyController.cs

@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Keys/{key}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RevokeKey([FromRoute] string? key)
+        public ActionResult RevokeKey([FromRoute, Required] string? key)
         {
             _sessionManager.RevokeToken(key);
             return NoContent();

+ 3 - 2
Jellyfin.Api/Controllers/CollectionController.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
@@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
+        public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
         {
             _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpDelete("{collectionId}/Items")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
+        public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
         {
             _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
             return NoContent();

+ 3 - 3
Jellyfin.Api/Controllers/ConfigurationController.cs

@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
 using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -9,7 +10,6 @@ using MediaBrowser.Model.Configuration;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Configuration")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
+        public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
         {
             _configurationManager.ReplaceConfiguration(configuration);
             return NoContent();
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("MediaEncoder/Path")]
         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
+        public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath)
         {
             _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
             return NoContent();

+ 7 - 7
Jellyfin.Api/Controllers/DevicesController.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Security;
@@ -8,7 +9,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+        public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
         {
             var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
             return _deviceManager.GetDevices(deviceQuery);
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string? id)
+        public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
         {
             var deviceInfo = _deviceManager.GetDevice(id);
             if (deviceInfo == null)
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string? id)
+        public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
         {
             var deviceInfo = _deviceManager.GetDeviceOptions(id);
             if (deviceInfo == null)
@@ -111,8 +111,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateDeviceOptions(
-            [FromQuery, BindRequired] string? id,
-            [FromBody, BindRequired] DeviceOptions deviceOptions)
+            [FromQuery, Required] string? id,
+            [FromBody, Required] DeviceOptions deviceOptions)
         {
             var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
             if (existingDeviceOptions == null)
@@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult DeleteDevice([FromQuery, BindRequired] string? id)
+        public ActionResult DeleteDevice([FromQuery, Required] string? id)
         {
             var existingDevice = _deviceManager.GetDevice(id);
             if (existingDevice == null)

+ 3 - 4
Jellyfin.Api/Controllers/DisplayPreferencesController.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -99,9 +98,9 @@ namespace Jellyfin.Api.Controllers
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
         public ActionResult UpdateDisplayPreferences(
             [FromRoute] string? displayPreferencesId,
-            [FromQuery, BindRequired] Guid userId,
-            [FromQuery, BindRequired] string? client,
-            [FromBody, BindRequired] DisplayPreferencesDto displayPreferences)
+            [FromQuery, Required] Guid userId,
+            [FromQuery, Required] string? client,
+            [FromBody, Required] DisplayPreferencesDto displayPreferences)
         {
             HomeSectionType[] defaults =
             {

+ 4 - 4
Jellyfin.Api/Controllers/EnvironmentController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using Jellyfin.Api.Constants;
@@ -8,7 +9,6 @@ using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("DirectoryContents")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
-            [FromQuery, BindRequired] string path,
+            [FromQuery, Required] string path,
             [FromQuery] bool includeFiles = false,
             [FromQuery] bool includeDirectories = false)
         {
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("ValidatePath")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto)
+        public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
         {
             if (validatePathDto.IsFile.HasValue)
             {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Parent path.</returns>
         [HttpGet("ParentPath")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<string?> GetParentPath([FromQuery, BindRequired] string path)
+        public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
         {
             string? parent = Path.GetDirectoryName(path);
             if (string.IsNullOrEmpty(parent))

+ 6 - 5
Jellyfin.Api/Controllers/ImageByNameController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using System.Net.Mime;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string? name, [FromRoute] string? type)
+        public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
         {
             var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
                 ? "folder"
@@ -110,8 +111,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetRatingImage(
-            [FromRoute] string? theme,
-            [FromRoute] string? name)
+            [FromRoute, Required] string? theme,
+            [FromRoute, Required] string? name)
         {
             return GetImageFile(_applicationPaths.RatingsPath, theme, name);
         }
@@ -143,8 +144,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<FileStreamResult> GetMediaInfoImage(
-            [FromRoute] string? theme,
-            [FromRoute] string? name)
+            [FromRoute, Required] string? theme,
+            [FromRoute, Required] string? name)
         {
             return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
         }

+ 2 - 0
Jellyfin.Api/Controllers/ImageController.cs

@@ -84,6 +84,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Users/{userId}/Images/{imageType}")]
         [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -259,6 +260,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpGet("Items/{itemId}/Images")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)

+ 2 - 1
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
@@ -174,7 +175,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
-            [FromRoute] string? name,
+            [FromRoute, Required] string? name,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] string? fields,

+ 10 - 11
Jellyfin.Api/Controllers/ItemLookupController.cs

@@ -22,7 +22,6 @@ using MediaBrowser.Model.Providers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.Controllers
@@ -94,7 +93,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Movie")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MovieInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -111,7 +110,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Trailer")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<TrailerInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -128,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicVideo")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MusicVideoInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -145,7 +144,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Series")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<SeriesInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -162,7 +161,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/BoxSet")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BoxSetInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -179,7 +178,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicArtist")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<ArtistInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -196,7 +195,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/MusicAlbum")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<AlbumInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -214,7 +213,7 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [HttpPost("Items/RemoteSearch/Person")]
         [Authorize(Policy = Policies.RequiresElevation)]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<PersonLookupInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -231,7 +230,7 @@ namespace Jellyfin.Api.Controllers
         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
         /// </returns>
         [HttpPost("Items/RemoteSearch/Book")]
-        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BookInfo> query)
+        public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
         {
             var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
                 .ConfigureAwait(false);
@@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         public async Task<ActionResult> ApplySearchCriteria(
             [FromRoute] Guid itemId,
-            [FromBody, BindRequired] RemoteSearchResult searchResult,
+            [FromBody, Required] RemoteSearchResult searchResult,
             [FromQuery] bool replaceAllImages = true)
         {
             var item = _libraryManager.GetItemById(itemId);

+ 3 - 3
Jellyfin.Api/Controllers/ItemUpdateController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;
@@ -17,7 +18,6 @@ using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -67,7 +67,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Items/{itemId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request)
+        public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Items/{itemId}/ContentType")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType)
+        public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)

+ 5 - 4
Jellyfin.Api/Controllers/LibraryController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -32,7 +33,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.Extensions.Logging;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Movie = Jellyfin.Data.Entities.Movie;
@@ -597,7 +597,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates)
+        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
         {
             foreach (var item in updates)
             {
@@ -685,6 +685,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
         [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
         [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
             [FromRoute] Guid itemId,
@@ -736,11 +737,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Library options info returned.</response>
         /// <returns>Library options info.</returns>
         [HttpGet("Libraries/AvailableOptions")]
-        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
             [FromQuery] string? libraryContentType,
-            [FromQuery] bool isNewLibrary = false)
+            [FromQuery] bool isNewLibrary)
         {
             var result = new LibraryOptionsResultDto();
 

+ 2 - 2
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -17,7 +18,6 @@ using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Paths")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddMediaPath(
-            [FromBody, BindRequired] MediaPathDto mediaPathDto,
+            [FromBody, Required] MediaPathDto mediaPathDto,
             [FromQuery] bool refreshLibrary = false)
         {
             _libraryMonitor.Stop();

+ 1 - 1
Jellyfin.Api/Controllers/LocalizationController.cs

@@ -11,7 +11,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Localization controller.
     /// </summary>
-    [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+    [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
     public class LocalizationController : BaseJellyfinApiController
     {
         private readonly ILocalizationManager _localization;

+ 3 - 2
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Buffers;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using System.Net.Mime;
@@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
         [HttpGet("Items/{itemId}/PlaybackInfo")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+        public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
         {
             return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
         }
@@ -281,7 +282,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
         [HttpPost("LiveStreams/Close")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CloseLiveStream([FromQuery] string? liveStreamId)
+        public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId)
         {
             _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
             return NoContent();

+ 9 - 5
Jellyfin.Api/Controllers/NotificationsController.cs

@@ -1,13 +1,16 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.NotificationDtos;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Notifications;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The notification controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class NotificationsController : BaseJellyfinApiController
     {
         private readonly INotificationManager _notificationManager;
@@ -83,19 +87,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Sends a notification to all admins.
         /// </summary>
-        /// <param name="name">The name of the notification.</param>
-        /// <param name="description">The description of the notification.</param>
         /// <param name="url">The URL of the notification.</param>
         /// <param name="level">The level of the notification.</param>
+        /// <param name="name">The name of the notification.</param>
+        /// <param name="description">The description of the notification.</param>
         /// <response code="204">Notification sent.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult CreateAdminNotification(
-            [FromQuery] string? name,
-            [FromQuery] string? description,
             [FromQuery] string? url,
-            [FromQuery] NotificationLevel? level)
+            [FromQuery] NotificationLevel? level,
+            [FromQuery, Required] string name = "",
+            [FromQuery, Required] string description = "")
         {
             var notification = new NotificationRequest
             {

+ 0 - 1
Jellyfin.Api/Controllers/PackageController.cs

@@ -127,7 +127,6 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Package repositories returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
         [HttpGet("Repositories")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
         {

+ 3 - 0
Jellyfin.Api/Controllers/PersonsController.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Globalization;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Persons controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class PersonsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;

+ 2 - 2
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
@@ -14,7 +15,6 @@ using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
-            [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest)
+            [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
         {
             Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
             var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest

+ 2 - 2
Jellyfin.Api/Controllers/PluginsController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -13,7 +14,6 @@ using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("SecurityInfo")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
+        public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
         {
             return NoContent();
         }

+ 3 - 3
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
 using System.Net.Mime;
@@ -18,7 +19,6 @@ using MediaBrowser.Model.Providers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public async Task<ActionResult> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
+        public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
         {
             var urlHash = imageUrl.GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
@@ -209,7 +209,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> DownloadRemoteImage(
             [FromRoute] Guid itemId,
-            [FromQuery, BindRequired] ImageType type,
+            [FromQuery, Required] ImageType type,
             [FromQuery] string? imageUrl)
         {
             var item = _libraryManager.GetItemById(itemId);

+ 5 - 5
Jellyfin.Api/Controllers/ScheduledTasksController.cs

@@ -1,12 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Model.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("{taskId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<TaskInfo> GetTask([FromRoute] string? taskId)
+        public ActionResult<TaskInfo> GetTask([FromRoute, Required] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
                 string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Running/{taskId}")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult StopTask([FromRoute] string? taskId)
+        public ActionResult StopTask([FromRoute, Required] string? taskId)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -144,8 +144,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult UpdateTask(
-            [FromRoute] string? taskId,
-            [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
+            [FromRoute, Required] string? taskId,
+            [FromBody, Required] TaskTriggerInfo[] triggerInfos)
         {
             var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
                 o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));

+ 32 - 17
Jellyfin.Api/Controllers/SessionController.cs

@@ -122,12 +122,13 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Viewing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult DisplayContent(
-            [FromRoute] string? sessionId,
-            [FromQuery] string? itemType,
-            [FromQuery] string? itemId,
-            [FromQuery] string? itemName)
+            [FromRoute, Required] string? sessionId,
+            [FromQuery, Required] string? itemType,
+            [FromQuery, Required] string? itemId,
+            [FromQuery, Required] string? itemName)
         {
             var command = new BrowseRequest
             {
@@ -156,9 +157,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Playing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromQuery] Guid[] itemIds,
             [FromQuery] long? startPositionTicks,
             [FromQuery] PlayCommand playCommand,
@@ -190,9 +192,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Playstate command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Playing/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromBody] PlaystateRequest playstateRequest)
         {
             _sessionManager.SendPlaystateCommand(
@@ -212,10 +215,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">System command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/System/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
-            [FromRoute] string? sessionId,
-            [FromRoute] string? command)
+            [FromRoute, Required] string? sessionId,
+            [FromRoute, Required] string? command)
         {
             var name = command;
             if (Enum.TryParse(name, true, out GeneralCommandType commandType))
@@ -243,10 +247,11 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">General command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Command/{command}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
-            [FromRoute] string? sessionId,
-            [FromRoute] string? command)
+            [FromRoute, Required] string? sessionId,
+            [FromRoute, Required] string? command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
@@ -269,9 +274,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Full general command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Command")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendFullGeneralCommand(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromBody, Required] GeneralCommand command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -302,11 +308,12 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Message sent.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/Message")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendMessageCommand(
-            [FromRoute] string? sessionId,
-            [FromQuery] string? text,
-            [FromQuery] string? header,
+            [FromRoute, Required] string? sessionId,
+            [FromQuery, Required] string? text,
+            [FromQuery, Required] string? header,
             [FromQuery] long? timeoutMs)
         {
             var command = new MessageCommand
@@ -329,9 +336,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">User added to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/{sessionId}/User/{userId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult AddUserToSession(
-            [FromRoute] string? sessionId,
+            [FromRoute, Required] string? sessionId,
             [FromRoute] Guid userId)
         {
             _sessionManager.AddAdditionalUser(sessionId, userId);
@@ -346,6 +354,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">User removed from session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpDelete("Sessions/{sessionId}/User/{userId}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RemoveUserFromSession(
             [FromRoute] string? sessionId,
@@ -367,9 +376,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Capabilities posted.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Capabilities")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostCapabilities(
-            [FromQuery] string? id,
+            [FromQuery, Required] string? id,
             [FromQuery] string? playableMediaTypes,
             [FromQuery] string? supportedCommands,
             [FromQuery] bool supportsMediaControl = false,
@@ -400,9 +410,10 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Capabilities updated.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Capabilities/Full")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult PostFullCapabilities(
-            [FromQuery] string? id,
+            [FromQuery, Required] string? id,
             [FromBody, Required] ClientCapabilities capabilities)
         {
             if (string.IsNullOrWhiteSpace(id))
@@ -423,6 +434,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Session reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Viewing")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportViewing(
             [FromQuery] string? sessionId,
@@ -440,6 +452,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Session end reported to server.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Logout")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportSessionEnded()
         {
@@ -455,6 +468,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Auth providers retrieved.</response>
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
         [HttpGet("Auth/Providers")]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
         {
@@ -468,6 +482,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
         [HttpGet("Auto/PasswordResetProviders")]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
         {
             return _userManager.GetPasswordResetProviders();

+ 3 - 3
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string? language,
+            [FromRoute, Required] string? language,
             [FromQuery] bool? isPerfectMatch)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DownloadRemoteSubtitles(
             [FromRoute] Guid itemId,
-            [FromRoute] string? subtitleId)
+            [FromRoute, Required] string? subtitleId)
         {
             var video = (Video)_libraryManager.GetItemById(itemId);
 
@@ -162,7 +162,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Produces(MediaTypeNames.Application.Octet)]
-        public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string? id)
+        public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
         {
             var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
 

+ 1 - 1
Jellyfin.Api/Controllers/SyncPlayController.cs

@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="filterItemId">Optional. Filter by item id.</param>
         /// <response code="200">Groups returned.</response>
-        /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
+        /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
         [HttpGet("List")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)

+ 2 - 5
Jellyfin.Api/Controllers/SystemController.cs

@@ -23,7 +23,6 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The system controller.
     /// </summary>
-    [Route("System")]
     public class SystemController : BaseJellyfinApiController
     {
         private readonly IServerApplicationHost _appHost;
@@ -60,8 +59,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Information retrieved.</response>
         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
         [HttpGet("Info")]
-        [Authorize(Policy = Policies.IgnoreSchedule)]
-        [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+        [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<SystemInfo>> GetSystemInfo()
         {
@@ -99,8 +97,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Server restarted.</response>
         /// <returns>No content. Server restarted.</returns>
         [HttpPost("Restart")]
-        [Authorize(Policy = Policies.LocalAccessOnly)]
-        [Authorize(Policy = Policies.RequiresElevation)]
+        [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult RestartApplication()
         {

+ 2 - 2
Jellyfin.Api/Controllers/TimeSyncController.cs

@@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The time sync controller.
     /// </summary>
-    [Route("GetUtcTime")]
+    [Route("")]
     public class TimeSyncController : BaseJellyfinApiController
     {
         /// <summary>
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="200">Time returned.</response>
         /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
-        [HttpGet]
+        [HttpGet("GetUtcTime")]
         [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
         public ActionResult<UtcTimeResponse> GetUtcTime()
         {

+ 7 - 6
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
@@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("NextUp")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
-            [FromQuery] Guid? userId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Upcoming")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
-            [FromQuery] Guid? userId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? fields,
@@ -193,8 +194,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
-            [FromRoute] string? seriesId,
-            [FromQuery] Guid? userId,
+            [FromRoute, Required] string? seriesId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] int? season,
             [FromQuery] string? seasonId,
@@ -316,8 +317,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
-            [FromRoute] string? seriesId,
-            [FromQuery] Guid? userId,
+            [FromRoute, Required] string? seriesId,
+            [FromQuery, Required] Guid? userId,
             [FromQuery] string? fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,

+ 5 - 6
Jellyfin.Api/Controllers/UserController.cs

@@ -20,7 +20,6 @@ using MediaBrowser.Model.Users;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -106,7 +105,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">User not found.</response>
         /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
         [HttpGet("{userId}")]
-        [Authorize(Policy = Policies.IgnoreSchedule)]
+        [Authorize(Policy = Policies.IgnoreParentalControl)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
@@ -157,8 +156,8 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
             [FromRoute, Required] Guid userId,
-            [FromQuery, BindRequired] string? pw,
-            [FromQuery, BindRequired] string? password)
+            [FromQuery, Required] string? pw,
+            [FromQuery] string? password)
         {
             var user = _userManager.GetUserById(userId);
 
@@ -190,7 +189,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
         [HttpPost("AuthenticateByName")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request)
+        public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
         {
             var auth = _authContext.GetAuthorizationInfo(Request);
 
@@ -371,7 +370,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User policy update forbidden.</response>
         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
         [HttpPost("{userId}/Policy")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]

+ 4 - 6
Jellyfin.Api/Controllers/VideoAttachmentsController.cs

@@ -1,12 +1,11 @@
 using System;
+using System.ComponentModel.DataAnnotations;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -16,7 +15,6 @@ namespace Jellyfin.Api.Controllers
     /// Attachments controller.
     /// </summary>
     [Route("Videos")]
-    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class VideoAttachmentsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
@@ -49,9 +47,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult<FileStreamResult>> GetAttachment(
-            [FromRoute] Guid videoId,
-            [FromRoute] string? mediaSourceId,
-            [FromRoute] int index)
+            [FromRoute, Required] Guid videoId,
+            [FromRoute, Required] string mediaSourceId,
+            [FromRoute, Required] int index)
         {
             try
             {

+ 2 - 2
Jellyfin.Api/Controllers/VideosController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using System.Net.Http;
@@ -35,7 +36,6 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// The videos controller.
     /// </summary>
-    [Route("Videos")]
     public class VideosController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status400BadRequest)]
-        public ActionResult MergeVersions([FromQuery] string? itemIds)
+        public ActionResult MergeVersions([FromQuery, Required] string? itemIds)
         {
             var items = RequestHelpers.Split(itemIds, ',', true)
                 .Select(i => _libraryManager.GetItemById(i))

+ 3 - 0
Jellyfin.Api/Controllers/YearsController.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
     /// <summary>
     /// Years controller.
     /// </summary>
+    [Authorize(Policy = Policies.DefaultAuthorization)]
     public class YearsController : BaseJellyfinApiController
     {
         private readonly ILibraryManager _libraryManager;

+ 1 - 1
Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs

@@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos
         /// <summary>
         /// Gets or sets UI language culture.
         /// </summary>
-        public string? UICulture { get; set; }
+        public string UICulture { get; set; } = null!;
 
         /// <summary>
         /// Gets or sets the metadata country code.

+ 31 - 4
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -7,8 +7,11 @@ using Jellyfin.Api;
 using Jellyfin.Api.Auth;
 using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 using Jellyfin.Api.Auth.DownloadPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
 using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
-using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
@@ -41,9 +44,12 @@ namespace Jellyfin.Server.Extensions
         {
             serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
-            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlOrFirstTimeSetupHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
             return serviceCollection.AddAuthorizationCore(options =>
             {
@@ -61,6 +67,13 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new DownloadRequirement());
                     });
+                options.AddPolicy(
+                    Policies.FirstTimeSetupOrDefault,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement());
+                    });
                 options.AddPolicy(
                     Policies.FirstTimeSetupOrElevated,
                     policy =>
@@ -69,11 +82,18 @@ namespace Jellyfin.Server.Extensions
                         policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
                     });
                 options.AddPolicy(
-                    Policies.IgnoreSchedule,
+                    Policies.IgnoreParentalControl,
                     policy =>
                     {
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
-                        policy.AddRequirements(new IgnoreScheduleRequirement());
+                        policy.AddRequirements(new IgnoreParentalControlRequirement());
+                    });
+                options.AddPolicy(
+                    Policies.IgnoreParentalControlOrFirstTimeSetup,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement());
                     });
                 options.AddPolicy(
                     Policies.LocalAccessOnly,
@@ -82,6 +102,13 @@ namespace Jellyfin.Server.Extensions
                         policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
                         policy.AddRequirements(new LocalAccessRequirement());
                     });
+                options.AddPolicy(
+                    Policies.LocalAccessOrRequiresElevation,
+                    policy =>
+                    {
+                        policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+                        policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement());
+                    });
                 options.AddPolicy(
                     Policies.RequiresElevation,
                     policy =>

+ 4 - 4
tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
     {
         private readonly Mock<IConfigurationManager> _configurationManagerMock;
         private readonly List<IAuthorizationRequirement> _requirements;
-        private readonly IgnoreScheduleHandler _sut;
+        private readonly IgnoreParentalControlHandler _sut;
         private readonly Mock<IUserManager> _userManagerMock;
         private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
 
@@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
         {
             var fixture = new Fixture().Customize(new AutoMoqCustomization());
             _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
-            _requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
+            _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() };
             _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
             _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
 
-            _sut = fixture.Create<IgnoreScheduleHandler>();
+            _sut = fixture.Create<IgnoreParentalControlHandler>();
         }
 
         [Theory]