Prevent unauthenticated re-run of the startup wizard on misconfiguration
This commit is contained in:
@@ -39,6 +39,8 @@ using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Drawing;
|
||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||
using Jellyfin.Networking.Manager;
|
||||
@@ -417,6 +419,8 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
Logger.LogInformation("Running startup tasks");
|
||||
|
||||
EnsureStartupWizardIntegrity();
|
||||
|
||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
|
||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||
@@ -436,6 +440,24 @@ namespace Emby.Server.Implementations
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void EnsureStartupWizardIntegrity()
|
||||
{
|
||||
if (ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hasConfiguredAdministrator = Resolve<IUserManager>().GetUsers()
|
||||
.Any(user => user.HasPermission(PermissionKind.IsAdministrator) && !string.IsNullOrEmpty(user.Password));
|
||||
|
||||
if (hasConfiguredAdministrator)
|
||||
{
|
||||
Logger.LogWarning("The startup wizard is marked incomplete but a configured administrator already exists. Marking setup as completed to prevent the unauthenticated setup endpoints from being reachable.");
|
||||
ConfigurationManager.Configuration.IsStartupWizardCompleted = true;
|
||||
ConfigurationManager.SaveConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Init(IServiceCollection serviceCollection)
|
||||
{
|
||||
|
||||
@@ -140,6 +140,11 @@ public class StartupController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
|
||||
{
|
||||
return BadRequest("Password must not be empty");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Api.Models.StartupDtos;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Api.Tests.Controllers;
|
||||
|
||||
public class StartupControllerTests
|
||||
{
|
||||
private readonly StartupController _subject;
|
||||
private readonly Mock<IUserManager> _mockUserManager;
|
||||
private readonly Mock<IServerConfigurationManager> _mockConfig;
|
||||
|
||||
public StartupControllerTests()
|
||||
{
|
||||
_mockUserManager = new Mock<IUserManager>();
|
||||
_mockConfig = new Mock<IServerConfigurationManager>();
|
||||
_subject = new StartupController(_mockConfig.Object, _mockUserManager.Object);
|
||||
}
|
||||
|
||||
private static User CreateUser()
|
||||
=> new User(
|
||||
"jellyfin",
|
||||
typeof(DefaultAuthenticationProvider).FullName!,
|
||||
typeof(DefaultPasswordResetProvider).FullName!);
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateStartupUser_WhenNoUserExists_ReturnsNotFound()
|
||||
{
|
||||
_mockUserManager.Setup(m => m.GetFirstUser()).Returns((User?)null);
|
||||
|
||||
var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "admin", Password = "pw" });
|
||||
|
||||
Assert.IsType<NotFoundResult>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateStartupUser_WhenPasswordAlreadyConfigured_ReturnsForbidden()
|
||||
{
|
||||
var user = CreateUser();
|
||||
user.Password = "already-set-hash";
|
||||
_mockUserManager.Setup(m => m.GetFirstUser()).Returns(user);
|
||||
|
||||
var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "attacker", Password = "new-pw" });
|
||||
|
||||
// The startup wizard must never overwrite the password of an already-provisioned
|
||||
// account, even if IsStartupWizardCompleted has been cleared.
|
||||
Assert.IsType<ForbidResult>(result);
|
||||
_mockUserManager.Verify(m => m.ChangePassword(It.IsAny<System.Guid>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateStartupUser_WhenNoPasswordYet_SetsPassword()
|
||||
{
|
||||
var user = CreateUser();
|
||||
Assert.True(string.IsNullOrEmpty(user.Password));
|
||||
_mockUserManager.Setup(m => m.GetFirstUser()).Returns(user);
|
||||
|
||||
var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "jellyfin", Password = "first-pw" });
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
_mockUserManager.Verify(m => m.ChangePassword(user.Id, "first-pw"), Times.Once);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user