Explorar el Código

Remove UserManager Cache (#10781)

* Remove redundant user cache

* Use DI for IPasswordResetProvider and IAuthenticationProvider
Patrick Barron hace 1 año
padre
commit
04dddd3a7b

+ 88 - 81
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -1,7 +1,7 @@
 #pragma warning disable CA1307
+#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
 
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
@@ -46,8 +46,6 @@ namespace Jellyfin.Server.Implementations.Users
         private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
         private readonly IServerConfigurationManager _serverConfigurationManager;
 
-        private readonly IDictionary<Guid, User> _users;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="UserManager"/> class.
         /// </summary>
@@ -58,6 +56,8 @@ namespace Jellyfin.Server.Implementations.Users
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</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(
             IDbContextFactory<JellyfinDbContext> dbProvider,
             IEventManager eventManager,
@@ -65,7 +65,9 @@ namespace Jellyfin.Server.Implementations.Users
             IApplicationHost appHost,
             IImageProcessor imageProcessor,
             ILogger<UserManager> logger,
-            IServerConfigurationManager serverConfigurationManager)
+            IServerConfigurationManager serverConfigurationManager,
+            IEnumerable<IPasswordResetProvider> passwordResetProviders,
+            IEnumerable<IAuthenticationProvider> authenticationProviders)
         {
             _dbProvider = dbProvider;
             _eventManager = eventManager;
@@ -75,35 +77,36 @@ namespace Jellyfin.Server.Implementations.Users
             _logger = logger;
             _serverConfigurationManager = serverConfigurationManager;
 
-            _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
-            _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
+            _passwordResetProviders = passwordResetProviders.ToList();
+            _authenticationProviders = authenticationProviders.ToList();
 
             _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
             _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().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/>
         public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 
         /// <inheritdoc/>
-        public IEnumerable<User> Users => _users.Values;
+        public IEnumerable<User> Users
+        {
+            get
+            {
+                using var dbContext = _dbProvider.CreateDbContext();
+                return GetUsersInternal(dbContext).ToList();
+            }
+        }
 
         /// <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 @
         // 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));
             }
 
-            _users.TryGetValue(id, out var user);
-            return user;
+            using var dbContext = _dbProvider.CreateDbContext();
+            return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
         }
 
         /// <inheritdoc/>
@@ -131,7 +134,9 @@ namespace Jellyfin.Server.Implementations.Users
                 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/>
@@ -196,8 +201,6 @@ namespace Jellyfin.Server.Implementations.Users
             user.AddDefaultPermissions();
             user.AddDefaultPreferences();
 
-            _users.Add(user.Id, user);
-
             return user;
         }
 
@@ -232,40 +235,46 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public async Task DeleteUserAsync(Guid userId)
         {
-            if (!_users.TryGetValue(userId, out var user))
-            {
-                throw new ResourceNotFoundException(nameof(userId));
-            }
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 
-            if (_users.Count == 1)
+            await using (dbContext.ConfigureAwait(false))
             {
-                throw new InvalidOperationException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "The user '{0}' cannot be deleted because there must be at least one user in the system.",
-                    user.Username));
-            }
+                var user = await dbContext.Users
+                    .AsSingleQuery()
+                    .Include(u => u.Permissions)
+                    .FirstOrDefaultAsync(u => u.Id.Equals(userId))
+                    .ConfigureAwait(false);
+                if (user is null)
+                {
+                    throw new ResourceNotFoundException(nameof(userId));
+                }
 
-            if (user.HasPermission(PermissionKind.IsAdministrator)
-                && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
-            {
-                throw new ArgumentException(
-                    string.Format(
+                if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
+                {
+                    throw new InvalidOperationException(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));
-            }
+                        "The user '{0}' cannot be deleted because there must be at least one user in the system.",
+                        user.Username));
+                }
+
+                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);
                 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/>
@@ -532,23 +541,23 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public async Task InitializeAsync()
         {
-            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
-            if (_users.Any())
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            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;
-            if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
-            {
-                defaultName = "MyJellyfinUser";
-            }
+                var defaultName = Environment.UserName;
+                if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
+                {
+                    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);
                 newUser.SetPermission(PermissionKind.IsAdministrator, true);
                 newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@@ -595,12 +604,9 @@ namespace Jellyfin.Server.Implementations.Users
             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
             await using (dbContext.ConfigureAwait(false))
             {
-                var user = dbContext.Users
-                               .Include(u => u.Permissions)
-                               .Include(u => u.Preferences)
-                               .Include(u => u.AccessSchedules)
-                               .Include(u => u.ProfileImage)
-                               .FirstOrDefault(u => u.Id.Equals(userId))
+                var user = await GetUsersInternal(dbContext)
+                    .FirstOrDefaultAsync(u => u.Id.Equals(userId))
+                    .ConfigureAwait(false)
                            ?? throw new ArgumentException("No user exists with given Id!");
 
                 user.SubtitleMode = config.SubtitleMode;
@@ -628,7 +634,6 @@ namespace Jellyfin.Server.Implementations.Users
                 user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 
                 dbContext.Update(user);
-                _users[user.Id] = user;
                 await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
         }
@@ -639,12 +644,9 @@ namespace Jellyfin.Server.Implementations.Users
             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
             await using (dbContext.ConfigureAwait(false))
             {
-                var user = dbContext.Users
-                               .Include(u => u.Permissions)
-                               .Include(u => u.Preferences)
-                               .Include(u => u.AccessSchedules)
-                               .Include(u => u.ProfileImage)
-                               .FirstOrDefault(u => u.Id.Equals(userId))
+                var user = await GetUsersInternal(dbContext)
+                    .FirstOrDefaultAsync(u => u.Id.Equals(userId))
+                    .ConfigureAwait(false)
                            ?? 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"
@@ -704,7 +706,6 @@ namespace Jellyfin.Server.Implementations.Users
                 user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
 
                 dbContext.Update(user);
-                _users[user.Id] = user;
                 await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
         }
@@ -725,7 +726,6 @@ namespace Jellyfin.Server.Implementations.Users
             }
 
             user.ProfileImage = null;
-            _users[user.Id] = user;
         }
 
         internal static void ThrowIfInvalidUsername(string name)
@@ -872,8 +872,15 @@ namespace Jellyfin.Server.Implementations.Users
         private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
         {
             dbContext.Users.Update(user);
-            _users[user.Id] = user;
             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);
     }
 }

+ 4 - 0
Jellyfin.Server/CoreAppHost.cs

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