diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index d4d40da577..5e5e25e847 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Auth new Claim(InternalClaimTypes.Device, authorizationInfo.Device), new Claim(InternalClaimTypes.Client, authorizationInfo.Client), new Claim(InternalClaimTypes.Version, authorizationInfo.Version), - new Claim(InternalClaimTypes.Token, authorizationInfo.Token) + new Claim(InternalClaimTypes.Token, authorizationInfo.Token), }; var identity = new ClaimsIdentity(claims, Scheme.Name); diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index 0b12f7d3c2..decbe0c035 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -38,6 +38,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { context.Succeed(firstTimeSetupOrElevatedRequirement); + return Task.CompletedTask; } var validated = ValidateClaims(context.User); diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 362d41b015..4ea5094b66 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Security.Claims; -using System.Text.Encodings.Web; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; @@ -9,7 +8,6 @@ using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth private readonly IFixture _fixture; private readonly Mock _jellyfinAuthServiceMock; - private readonly Mock> _optionsMonitorMock; - private readonly Mock _clockMock; - private readonly Mock _serviceProviderMock; - private readonly Mock _authenticationServiceMock; - private readonly UrlEncoder _urlEncoder; - private readonly HttpContext _context; private readonly CustomAuthenticationHandler _sut; private readonly AuthenticationScheme _scheme; @@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth AllowFixtureCircularDependencies(); _jellyfinAuthServiceMock = _fixture.Freeze>(); - _optionsMonitorMock = _fixture.Freeze>>(); - _clockMock = _fixture.Freeze>(); - _serviceProviderMock = _fixture.Freeze>(); - _authenticationServiceMock = _fixture.Freeze>(); + var optionsMonitorMock = _fixture.Freeze>>(); + var serviceProviderMock = _fixture.Freeze>(); + var authenticationServiceMock = _fixture.Freeze>(); _fixture.Register(() => new NullLoggerFactory()); - _urlEncoder = UrlEncoder.Default; + serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) + .Returns(authenticationServiceMock.Object); - _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) - .Returns(_authenticationServiceMock.Object); - - _optionsMonitorMock.Setup(o => o.Get(It.IsAny())) + optionsMonitorMock.Setup(o => o.Get(It.IsAny())) .Returns(new AuthenticationSchemeOptions { ForwardAuthenticate = null }); - _context = new DefaultHttpContext + HttpContext context = new DefaultHttpContext { - RequestServices = _serviceProviderMock.Object + RequestServices = serviceProviderMock.Object }; _scheme = new AuthenticationScheme( @@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth typeof(CustomAuthenticationHandler)); _sut = _fixture.Create(); - _sut.InitializeAsync(_scheme, _context).Wait(); - } - - [Fact] - public async Task HandleAuthenticateAsyncShouldFailWithNullUser() - { - _jellyfinAuthServiceMock.Setup( - a => a.Authenticate( - It.IsAny(), - It.IsAny())) - .Returns((User?)null); - - var authenticateResult = await _sut.AuthenticateAsync(); - - Assert.False(authenticateResult.Succeeded); - Assert.True(authenticateResult.None); - // TODO return when legacy API is removed. - // Assert.Equal("Invalid user", authenticateResult.Failure.Message); + _sut.InitializeAsync(_scheme, context).Wait(); } [Fact] @@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth _jellyfinAuthServiceMock.Setup( a => a.Authenticate( - It.IsAny(), - It.IsAny())) + It.IsAny())) .Throws(new SecurityException(errorMessage)); var authenticateResult = await _sut.AuthenticateAsync(); @@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth [Fact] public async Task HandleAuthenticateAsyncShouldAssignNameClaim() { - var user = SetupUser(); + var authorizationInfo = SetupUser(); var authenticateResult = await _sut.AuthenticateAsync(); - Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username)); + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username)); } [Theory] @@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth [InlineData(false)] public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin) { - var user = SetupUser(isAdmin); + var authorizationInfo = SetupUser(isAdmin); var authenticateResult = await _sut.AuthenticateAsync(); - var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; + var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); } @@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); } - private User SetupUser(bool isAdmin = false) + private AuthorizationInfo SetupUser(bool isAdmin = false) { - var user = _fixture.Create(); - user.SetPermission(PermissionKind.IsAdministrator, isAdmin); + var authorizationInfo = _fixture.Create(); + authorizationInfo.User = _fixture.Create(); + authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); _jellyfinAuthServiceMock.Setup( a => a.Authenticate( - It.IsAny(), - It.IsAny())) - .Returns(user); + It.IsAny())) + .Returns(authorizationInfo); - return user; + return authorizationInfo; } private void AllowFixtureCircularDependencies() diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs index e40af703f9..e455df6435 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -1,13 +1,21 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Net; using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Moq; using Xunit; @@ -15,15 +23,28 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy { public class FirstTimeSetupOrElevatedHandlerTests { + /// + /// 127.0.0.1. + /// + private const long InternalIp = 16777343; + + /// + /// 1.1.1.1. + /// + /// private const long ExternalIp = 16843009; private readonly Mock _configurationManagerMock; private readonly List _requirements; private readonly FirstTimeSetupOrElevatedHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; public FirstTimeSetupOrElevatedHandlerTests() { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); _requirements = new List { new FirstTimeSetupOrElevatedRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); _sut = fixture.Create(); } @@ -35,8 +56,15 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) { SetupConfigurationManager(false); - var user = SetupUser(userRole); - var context = new AuthorizationHandlerContext(_requirements, user, null); + var (user, claims) = SetupUser(userRole); + + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.True(context.HasSucceeded); @@ -49,18 +77,42 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) { SetupConfigurationManager(true); - var user = SetupUser(userRole); - var context = new AuthorizationHandlerContext(_requirements, user, null); + var (user, claims) = SetupUser(userRole); + + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); + + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } - private static ClaimsPrincipal SetupUser(string role) + private static (User, ClaimsPrincipal) SetupUser(string role) { - var claims = new[] { new Claim(ClaimTypes.Role, role) }; + var user = new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName, + typeof(DefaultPasswordResetProvider).FullName); + + user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim(ClaimTypes.Name, "jellyfin"), + new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.Device, "test"), + new Claim(InternalClaimTypes.Client, "test"), + new Claim(InternalClaimTypes.Version, "test"), + new Claim(InternalClaimTypes.Token, "test"), + }; + var identity = new ClaimsIdentity(claims); - return new ClaimsPrincipal(identity); + return (user, new ClaimsPrincipal(identity)); } private void SetupConfigurationManager(bool startupWizardCompleted) diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs index cd05a8328d..58eae84789 100644 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -1,20 +1,48 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Net; using System.Security.Claims; using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Users; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Moq; using Xunit; namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy { public class RequiresElevationHandlerTests { + /// + /// 127.0.0.1. + /// + private const long InternalIp = 16777343; + + private readonly Mock _configurationManagerMock; + private readonly List _requirements; private readonly RequiresElevationHandler _sut; + private readonly Mock _userManagerMock; + private readonly Mock _httpContextAccessor; public RequiresElevationHandlerTests() { - _sut = new RequiresElevationHandler(); + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List { new RequiresElevationRequirement() }; + _userManagerMock = fixture.Freeze>(); + _httpContextAccessor = fixture.Freeze>(); + + _sut = fixture.Create(); } [Theory] @@ -23,16 +51,54 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy [InlineData(UserRoles.Guest, false)] public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) { - var requirements = new List { new RequiresElevationRequirement() }; + SetupConfigurationManager(true); + var (user, claims) = SetupUser(role); - var claims = new[] { new Claim(ClaimTypes.Role, role) }; - var identity = new ClaimsIdentity(claims); - var user = new ClaimsPrincipal(identity); + _userManagerMock.Setup(u => u.GetUserById(It.IsAny())) + .Returns(user); + + _httpContextAccessor.Setup(h => h.HttpContext.Connection.RemoteIpAddress) + .Returns(new IPAddress(InternalIp)); - var context = new AuthorizationHandlerContext(requirements, user, null); + var context = new AuthorizationHandlerContext(_requirements, claims, null); await _sut.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } + + private static (User, ClaimsPrincipal) SetupUser(string role) + { + var user = new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName, + typeof(DefaultPasswordResetProvider).FullName); + + user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim(ClaimTypes.Name, "jellyfin"), + new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.Device, "test"), + new Claim(InternalClaimTypes.Client, "test"), + new Claim(InternalClaimTypes.Version, "test"), + new Claim(InternalClaimTypes.Token, "test"), + }; + + var identity = new ClaimsIdentity(claims); + return (user, new ClaimsPrincipal(identity)); + } + + private void SetupConfigurationManager(bool startupWizardCompleted) + { + var commonConfiguration = new BaseApplicationConfiguration + { + IsStartupWizardCompleted = startupWizardCompleted + }; + + _configurationManagerMock.Setup(c => c.CommonConfiguration) + .Returns(commonConfiguration); + } } } diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index aedcc7c42e..010fad520a 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -35,6 +35,7 @@ +