diff options
| author | Bond-009 <bond.009@outlook.com> | 2020-08-20 16:40:03 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-20 16:40:03 +0200 |
| commit | 5160e627f18fb4a763eaa77b836d20486e55c5e9 (patch) | |
| tree | 5fb90ba0ee4d217384d31d1828b6a42a74168a45 /Jellyfin.Server.Implementations/Users/UserManager.cs | |
| parent | 3588ee5229b76bca9417813e208e86492e06d609 (diff) | |
| parent | 250e351613e0eed7977c8cdad4a9078927458feb (diff) | |
Merge branch 'master' into feature/ffmpeg-version-check
Diffstat (limited to 'Jellyfin.Server.Implementations/Users/UserManager.cs')
| -rw-r--r-- | Jellyfin.Server.Implementations/Users/UserManager.cs | 197 |
1 files changed, 130 insertions, 67 deletions
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ae5c311bf..11402ee05 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -23,6 +23,7 @@ using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Users; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.Users @@ -38,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; private readonly ILogger<UserManager> _logger; - - private IAuthenticationProvider[] _authenticationProviders = null!; - private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!; - private InvalidAuthProvider _invalidAuthProvider = null!; - private IPasswordResetProvider[] _passwordResetProviders = null!; - private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!; + private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders; + private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders; + private readonly InvalidAuthProvider _invalidAuthProvider; + private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; + private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; /// <summary> /// Initializes a new instance of the <see cref="UserManager"/> class. @@ -68,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users _appHost = appHost; _imageProcessor = imageProcessor; _logger = logger; + + _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); + _authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); + + _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); + _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); + _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); } /// <inheritdoc/> @@ -86,10 +93,31 @@ namespace Jellyfin.Server.Implementations.Users public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut; /// <inheritdoc/> - public IEnumerable<User> Users => _dbProvider.CreateContext().Users; + public IEnumerable<User> Users + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .ToList(); + } + } /// <inheritdoc/> - public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id); + public IEnumerable<Guid> UsersIds + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Select(user => user.Id) + .ToList(); + } + } /// <inheritdoc/> public User? GetUserById(Guid id) @@ -99,7 +127,13 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Guid can't be empty", nameof(id)); } - return _dbProvider.CreateContext().Users.Find(id); + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .FirstOrDefault(user => user.Id == id); } /// <inheritdoc/> @@ -110,9 +144,14 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Invalid username", nameof(name)); } - // This can't use an overload with StringComparer because that would cause the query to - // have to be evaluated client-side. - return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name)); + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .AsEnumerable() + .FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); } /// <inheritdoc/> @@ -133,7 +172,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("The new and old names must be different."); } - if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal))) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, @@ -150,7 +189,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdateUser(User user) { - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Update(user); dbContext.SaveChanges(); } @@ -158,34 +197,42 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task UpdateUserAsync(User user) { - var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Update(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); } - /// <inheritdoc/> - public User CreateUser(string name) + internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext) { - if (!IsValidUsername(name)) - { - throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); - } - - var dbContext = _dbProvider.CreateContext(); - // TODO: Remove after user item data is migrated. - var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0; + var max = await dbContext.Users.AnyAsync().ConfigureAwait(false) + ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) + : 0; - var newUser = new User( + return new User( name, _defaultAuthenticationProvider.GetType().FullName, _defaultPasswordResetProvider.GetType().FullName) { InternalId = max + 1 }; + } + + /// <inheritdoc/> + public async Task<User> CreateUserAsync(string name) + { + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + using var dbContext = _dbProvider.CreateContext(); + + var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); + dbContext.Users.Add(newUser); - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync().ConfigureAwait(false); OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser)); @@ -195,8 +242,13 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void DeleteUser(Guid userId) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId); if (user == null) { throw new ResourceNotFoundException(nameof(userId)); @@ -273,7 +325,17 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) { - GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); + if (newPassword != null) + { + newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString(); + } + + if (string.IsNullOrWhiteSpace(newPasswordSha1)) + { + throw new ArgumentNullException(nameof(newPasswordSha1)); + } + + user.EasyPassword = newPasswordSha1; UpdateUser(user); OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user)); @@ -371,7 +433,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentNullException(nameof(username)); } - var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); bool success; IAuthenticationProvider? authenticationProvider; @@ -399,8 +461,7 @@ namespace Jellyfin.Server.Implementations.Users // Search the database for the user again // the authentication provider might have created it - user = Users - .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) { @@ -474,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users } else { - IncrementInvalidLoginAttemptCount(user); + await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); _logger.LogInformation( "Authentication request for {UserName} has been denied (IP: {IP}).", user.Username, @@ -492,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users if (user != null && isInNetwork) { var passwordResetProvider = GetPasswordResetProvider(user); - return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); + var result = await passwordResetProvider + .StartForgotPasswordProcess(user, isInNetwork) + .ConfigureAwait(false); + + await UpdateUserAsync(user).ConfigureAwait(false); + return result; } return new ForgotPasswordResult @@ -522,48 +588,32 @@ namespace Jellyfin.Server.Implementations.Users }; } - /// <inheritdoc/> - public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - _passwordResetProviders = passwordResetProviders.ToArray(); - - _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); - _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); - _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); - } - /// <inheritdoc /> - public void Initialize() + public async Task InitializeAsync() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); - if (dbContext.Users.Any()) + if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) { return; } var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) + if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName)) { defaultName = "MyJellyfinUser"; } _logger.LogWarning("No users, creating one with username {UserName}", defaultName); - if (!IsValidUsername(defaultName)) - { - throw new ArgumentException("Provided username is not valid!", defaultName); - } - - var newUser = CreateUser(defaultName); + var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); - dbContext.Users.Update(newUser); - dbContext.SaveChanges(); + dbContext.Users.Add(newUser); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// <inheritdoc/> @@ -599,8 +649,15 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdateConfiguration(Guid userId, UserConfiguration config) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!"); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId) + ?? throw new ArgumentException("No user exists with given Id!"); + user.SubtitleMode = config.SubtitleMode; user.HidePlayedInLatest = config.HidePlayedInLatest; user.EnableLocalPassword = config.EnableLocalPassword; @@ -625,8 +682,14 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdatePolicy(Guid userId, UserPolicy policy) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!"); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId) + ?? 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" int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch @@ -689,7 +752,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void ClearProfileImage(User user) { - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); dbContext.Remove(user.ProfileImage); dbContext.SaveChanges(); } @@ -698,8 +761,8 @@ namespace Jellyfin.Server.Implementations.Users { // 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 - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(name, @"^[\w\-'._@]*$"); + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) + return Regex.IsMatch(name, @"^[\w\ \-'._@]*$"); } private IAuthenticationProvider GetAuthenticationProvider(User user) @@ -831,7 +894,7 @@ namespace Jellyfin.Server.Implementations.Users } } - private void IncrementInvalidLoginAttemptCount(User user) + private async Task IncrementInvalidLoginAttemptCount(User user) { user.InvalidLoginAttemptCount++; int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; @@ -845,7 +908,7 @@ namespace Jellyfin.Server.Implementations.Users user.InvalidLoginAttemptCount); } - UpdateUser(user); + await UpdateUserAsync(user).ConfigureAwait(false); } } } |
