Remove UserManager Cache (#10781)

* Remove redundant user cache

* Use DI for IPasswordResetProvider and IAuthenticationProvider
pull/10823/head
Patrick Barron 5 months ago committed by GitHub
parent 5ff7c17daf
commit 04dddd3a7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,7 @@
#pragma warning disable CA1307 #pragma warning disable CA1307
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -46,8 +46,6 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary> /// </summary>
@ -58,6 +56,8 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="serverConfigurationManager">The system config manager.</param> /// <param name="serverConfigurationManager">The system config manager.</param>
/// <param name="passwordResetProviders">The password reset providers.</param>
/// <param name="authenticationProviders">The authentication providers.</param>
public UserManager( public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider, IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager, IEventManager eventManager,
@ -65,7 +65,9 @@ namespace Jellyfin.Server.Implementations.Users
IApplicationHost appHost, IApplicationHost appHost,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILogger<UserManager> logger, ILogger<UserManager> logger,
IServerConfigurationManager serverConfigurationManager) IServerConfigurationManager serverConfigurationManager,
IEnumerable<IPasswordResetProvider> passwordResetProviders,
IEnumerable<IAuthenticationProvider> authenticationProviders)
{ {
_dbProvider = dbProvider; _dbProvider = dbProvider;
_eventManager = eventManager; _eventManager = eventManager;
@ -75,35 +77,36 @@ namespace Jellyfin.Server.Implementations.Users
_logger = logger; _logger = logger;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); _passwordResetProviders = passwordResetProviders.ToList();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); _authenticationProviders = authenticationProviders.ToList();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<User> Users => _users.Values; public IEnumerable<User> Users
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).ToList();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Guid> UsersIds => _users.Keys; public IEnumerable<Guid> UsersIds
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return dbContext.Users.Select(u => u.Id).ToList();
}
}
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@ -119,8 +122,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
_users.TryGetValue(id, out var user); using var dbContext = _dbProvider.CreateDbContext();
return user; return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -131,7 +134,9 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name)); throw new ArgumentException("Invalid username", nameof(name));
} }
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext)
.FirstOrDefault(u => string.Equals(u.Username, name));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -196,8 +201,6 @@ namespace Jellyfin.Server.Implementations.Users
user.AddDefaultPermissions(); user.AddDefaultPermissions();
user.AddDefaultPreferences(); user.AddDefaultPreferences();
_users.Add(user.Id, user);
return user; return user;
} }
@ -232,40 +235,46 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId) public async Task DeleteUserAsync(Guid userId)
{ {
if (!_users.TryGetValue(userId, out var user)) var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
{
throw new ResourceNotFoundException(nameof(userId));
}
if (_users.Count == 1) await using (dbContext.ConfigureAwait(false))
{ {
throw new InvalidOperationException(string.Format( var user = await dbContext.Users
CultureInfo.InvariantCulture, .AsSingleQuery()
"The user '{0}' cannot be deleted because there must be at least one user in the system.", .Include(u => u.Permissions)
user.Username)); .FirstOrDefaultAsync(u => u.Id.Equals(userId))
} .ConfigureAwait(false);
if (user is null)
{
throw new ResourceNotFoundException(nameof(userId));
}
if (user.HasPermission(PermissionKind.IsAdministrator) if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) {
{ throw new InvalidOperationException(string.Format(
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.", "The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username), user.Username));
nameof(userId)); }
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& await dbContext.Users
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
.ConfigureAwait(false) == 1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(userId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Remove(user); dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
_users.Remove(userId); await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -532,23 +541,23 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc /> /// <inheritdoc />
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist. var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
if (_users.Any()) await using (dbContext.ConfigureAwait(false))
{ {
return; // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
} if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
var defaultName = Environment.UserName; var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{ {
defaultName = "MyJellyfinUser"; defaultName = "MyJellyfinUser";
} }
_logger.LogWarning("No users, creating one with username {UserName}", defaultName); _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@ -595,12 +604,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var user = await GetUsersInternal(dbContext)
.Include(u => u.Permissions) .FirstOrDefaultAsync(u => u.Id.Equals(userId))
.Include(u => u.Preferences) .ConfigureAwait(false)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode; user.SubtitleMode = config.SubtitleMode;
@ -628,7 +634,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -639,12 +644,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var user = await GetUsersInternal(dbContext)
.Include(u => u.Permissions) .FirstOrDefaultAsync(u => u.Id.Equals(userId))
.Include(u => u.Preferences) .ConfigureAwait(false)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@ -704,7 +706,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -725,7 +726,6 @@ namespace Jellyfin.Server.Implementations.Users
} }
user.ProfileImage = null; user.ProfileImage = null;
_users[user.Id] = user;
} }
internal static void ThrowIfInvalidUsername(string name) internal static void ThrowIfInvalidUsername(string name)
@ -872,8 +872,15 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{ {
dbContext.Users.Update(user); dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
=> dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage);
} }
} }

@ -14,6 +14,7 @@ using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users; using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -78,6 +79,9 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>();
serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>();
serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();

Loading…
Cancel
Save