aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api')
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs68
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs43
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs23
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs11
-rw-r--r--Jellyfin.Api/BaseJellyfinApiController.cs13
-rw-r--r--Jellyfin.Api/Constants/AuthenticationSchemes.cs13
-rw-r--r--Jellyfin.Api/Constants/Policies.cs18
-rw-r--r--Jellyfin.Api/Constants/UserRoles.cs23
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs127
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj32
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs23
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs18
-rw-r--r--Jellyfin.Api/MvcRoutePrefix.cs56
14 files changed, 479 insertions, 0 deletions
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
new file mode 100644
index 000000000..26f7d9d2d
--- /dev/null
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -0,0 +1,68 @@
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Jellyfin.Api.Auth
+{
+ /// <summary>
+ /// Custom authentication handler wrapping the legacy authentication.
+ /// </summary>
+ public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
+ {
+ private readonly IAuthService _authService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CustomAuthenticationHandler" /> class.
+ /// </summary>
+ /// <param name="authService">The jellyfin authentication service.</param>
+ /// <param name="options">Options monitor.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="encoder">The url encoder.</param>
+ /// <param name="clock">The system clock.</param>
+ public CustomAuthenticationHandler(
+ IAuthService authService,
+ IOptionsMonitor<AuthenticationSchemeOptions> options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock clock) : base(options, logger, encoder, clock)
+ {
+ _authService = authService;
+ }
+
+ /// <inheritdoc />
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var authenticatedAttribute = new AuthenticatedAttribute();
+ try
+ {
+ var user = _authService.Authenticate(Request, authenticatedAttribute);
+ if (user == null)
+ {
+ return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
+ }
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Name, user.Name),
+ new Claim(
+ ClaimTypes.Role,
+ value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User)
+ };
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, Scheme.Name);
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+ catch (SecurityException ex)
+ {
+ return Task.FromResult(AuthenticateResult.Fail(ex));
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
new file mode 100644
index 000000000..34aa5d12c
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -0,0 +1,43 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
+{
+ /// <summary>
+ /// Authorization handler for requiring first time setup or elevated privileges.
+ /// </summary>
+ public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+ {
+ private readonly IConfigurationManager _configurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
+ /// </summary>
+ /// <param name="configurationManager">The jellyfin configuration manager.</param>
+ public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
+ {
+ _configurationManager = configurationManager;
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement firstTimeSetupOrElevatedRequirement)
+ {
+ if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
+ {
+ context.Succeed(firstTimeSetupOrElevatedRequirement);
+ }
+ else if (context.User.IsInRole(UserRoles.Administrator))
+ {
+ context.Succeed(firstTimeSetupOrElevatedRequirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
new file mode 100644
index 000000000..51ba637b6
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
+{
+ /// <summary>
+ /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
+ /// </summary>
+ public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
new file mode 100644
index 000000000..2d3bb1aa4
--- /dev/null
+++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.RequiresElevationPolicy
+{
+ /// <summary>
+ /// Authorization handler for requiring elevated privileges.
+ /// </summary>
+ public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
+ {
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
+ {
+ if (context.User.IsInRole(UserRoles.Administrator))
+ {
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
new file mode 100644
index 000000000..cfff1cc0c
--- /dev/null
+++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.RequiresElevationPolicy
+{
+ /// <summary>
+ /// The authorization requirement for requiring elevated privileges in the authorization handler.
+ /// </summary>
+ public class RequiresElevationRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
new file mode 100644
index 000000000..1f4508e6c
--- /dev/null
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api
+{
+ /// <summary>
+ /// Base api controller for the API setting a default route.
+ /// </summary>
+ [ApiController]
+ [Route("[controller]")]
+ public class BaseJellyfinApiController : ControllerBase
+ {
+ }
+}
diff --git a/Jellyfin.Api/Constants/AuthenticationSchemes.cs b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
new file mode 100644
index 000000000..bac3379e7
--- /dev/null
+++ b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Api.Constants
+{
+ /// <summary>
+ /// Authentication schemes for user authentication in the API.
+ /// </summary>
+ public static class AuthenticationSchemes
+ {
+ /// <summary>
+ /// Scheme name for the custom legacy authentication.
+ /// </summary>
+ public const string CustomAuthentication = "CustomAuthentication";
+ }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
new file mode 100644
index 000000000..e2b383f75
--- /dev/null
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Constants
+{
+ /// <summary>
+ /// Policies for the API authorization.
+ /// </summary>
+ public static class Policies
+ {
+ /// <summary>
+ /// Policy name for requiring first time setup or elevated privileges.
+ /// </summary>
+ public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated";
+
+ /// <summary>
+ /// Policy name for requiring elevated privileges.
+ /// </summary>
+ public const string RequiresElevation = "RequiresElevation";
+ }
+}
diff --git a/Jellyfin.Api/Constants/UserRoles.cs b/Jellyfin.Api/Constants/UserRoles.cs
new file mode 100644
index 000000000..d9a536e7d
--- /dev/null
+++ b/Jellyfin.Api/Constants/UserRoles.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Constants
+{
+ /// <summary>
+ /// Constants for user roles used in the authentication and authorization for the API.
+ /// </summary>
+ public static class UserRoles
+ {
+ /// <summary>
+ /// Guest user.
+ /// </summary>
+ public const string Guest = "Guest";
+
+ /// <summary>
+ /// Regular user with no special privileges.
+ /// </summary>
+ public const string User = "User";
+
+ /// <summary>
+ /// Administrator user with elevated privileges.
+ /// </summary>
+ public const string Administrator = "Administrator";
+ }
+}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
new file mode 100644
index 000000000..1014c8c56
--- /dev/null
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -0,0 +1,127 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.StartupDtos;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// The startup wizard controller.
+ /// </summary>
+ [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+ public class StartupController : BaseJellyfinApiController
+ {
+ private readonly IServerConfigurationManager _config;
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StartupController" /> class.
+ /// </summary>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ public StartupController(IServerConfigurationManager config, IUserManager userManager)
+ {
+ _config = config;
+ _userManager = userManager;
+ }
+
+ /// <summary>
+ /// Api endpoint for completing the startup wizard.
+ /// </summary>
+ [HttpPost("Complete")]
+ public void CompleteWizard()
+ {
+ _config.Configuration.IsStartupWizardCompleted = true;
+ _config.SetOptimalValues();
+ _config.SaveConfiguration();
+ }
+
+ /// <summary>
+ /// Endpoint for getting the initial startup wizard configuration.
+ /// </summary>
+ /// <returns>The initial startup wizard configuration.</returns>
+ [HttpGet("Configuration")]
+ public StartupConfigurationDto GetStartupConfiguration()
+ {
+ var result = new StartupConfigurationDto
+ {
+ UICulture = _config.Configuration.UICulture,
+ MetadataCountryCode = _config.Configuration.MetadataCountryCode,
+ PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
+ };
+
+ return result;
+ }
+
+ /// <summary>
+ /// Endpoint for updating the initial startup wizard configuration.
+ /// </summary>
+ /// <param name="uiCulture">The UI language culture.</param>
+ /// <param name="metadataCountryCode">The metadata country code.</param>
+ /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
+ [HttpPost("Configuration")]
+ public void UpdateInitialConfiguration(
+ [FromForm] string uiCulture,
+ [FromForm] string metadataCountryCode,
+ [FromForm] string preferredMetadataLanguage)
+ {
+ _config.Configuration.UICulture = uiCulture;
+ _config.Configuration.MetadataCountryCode = metadataCountryCode;
+ _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
+ _config.SaveConfiguration();
+ }
+
+ /// <summary>
+ /// Endpoint for (dis)allowing remote access and UPnP.
+ /// </summary>
+ /// <param name="enableRemoteAccess">Enable remote access.</param>
+ /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
+ [HttpPost("RemoteAccess")]
+ public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
+ {
+ _config.Configuration.EnableRemoteAccess = enableRemoteAccess;
+ _config.Configuration.EnableUPnP = enableAutomaticPortMapping;
+ _config.SaveConfiguration();
+ }
+
+ /// <summary>
+ /// Endpoint for returning the first user.
+ /// </summary>
+ /// <returns>The first user.</returns>
+ [HttpGet("User")]
+ public StartupUserDto GetFirstUser()
+ {
+ var user = _userManager.Users.First();
+
+ return new StartupUserDto
+ {
+ Name = user.Name,
+ Password = user.Password
+ };
+ }
+
+ /// <summary>
+ /// Endpoint for updating the user name and password.
+ /// </summary>
+ /// <param name="startupUserDto">The DTO containing username and password.</param>
+ /// <returns>The async task.</returns>
+ [HttpPost("User")]
+ public async Task UpdateUser([FromForm] StartupUserDto startupUserDto)
+ {
+ var user = _userManager.Users.First();
+
+ user.Name = startupUserDto.Name;
+
+ _userManager.UpdateUser(user);
+
+ if (!string.IsNullOrEmpty(startupUserDto.Password))
+ {
+ await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
new file mode 100644
index 000000000..a2818b45d
--- /dev/null
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.1</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ </ItemGroup>
+
+ <!-- Code analysers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+</Project>
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
new file mode 100644
index 000000000..d048dad0a
--- /dev/null
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Api.Models.StartupDtos
+{
+ /// <summary>
+ /// The startup configuration DTO.
+ /// </summary>
+ public class StartupConfigurationDto
+ {
+ /// <summary>
+ /// Gets or sets UI language culture.
+ /// </summary>
+ public string UICulture { get; set; }
+
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ public string MetadataCountryCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preferred language for the metadata.
+ /// </summary>
+ public string PreferredMetadataLanguage { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
new file mode 100644
index 000000000..3a9348037
--- /dev/null
+++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Models.StartupDtos
+{
+ /// <summary>
+ /// The startup user DTO.
+ /// </summary>
+ public class StartupUserDto
+ {
+ /// <summary>
+ /// Gets or sets the username.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's password.
+ /// </summary>
+ public string Password { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/MvcRoutePrefix.cs b/Jellyfin.Api/MvcRoutePrefix.cs
new file mode 100644
index 000000000..e00973094
--- /dev/null
+++ b/Jellyfin.Api/MvcRoutePrefix.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationModels;
+
+namespace Jellyfin.Api
+{
+ /// <summary>
+ /// Route prefixing for ASP.NET MVC.
+ /// </summary>
+ public static class MvcRoutePrefix
+ {
+ /// <summary>
+ /// Adds route prefixes to the MVC conventions.
+ /// </summary>
+ /// <param name="opts">The MVC options.</param>
+ /// <param name="prefixes">The list of prefixes.</param>
+ public static void UseGeneralRoutePrefix(this MvcOptions opts, params string[] prefixes)
+ {
+ opts.Conventions.Insert(0, new RoutePrefixConvention(prefixes));
+ }
+
+ private class RoutePrefixConvention : IApplicationModelConvention
+ {
+ private readonly AttributeRouteModel[] _routePrefixes;
+
+ public RoutePrefixConvention(IEnumerable<string> prefixes)
+ {
+ _routePrefixes = prefixes.Select(p => new AttributeRouteModel(new RouteAttribute(p))).ToArray();
+ }
+
+ public void Apply(ApplicationModel application)
+ {
+ foreach (var controller in application.Controllers)
+ {
+ if (controller.Selectors == null)
+ {
+ continue;
+ }
+
+ var newSelectors = new List<SelectorModel>();
+ foreach (var selector in controller.Selectors)
+ {
+ newSelectors.AddRange(_routePrefixes.Select(routePrefix => new SelectorModel(selector)
+ {
+ AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(routePrefix, selector.AttributeRouteModel)
+ }));
+ }
+
+ controller.Selectors.Clear();
+ newSelectors.ForEach(selector => controller.Selectors.Add(selector));
+ }
+ }
+ }
+ }
+}