diff options
Diffstat (limited to 'Jellyfin.Api')
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)); + } + } + } + } +} |
