aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Users
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2021-12-23 19:38:10 -0700
committerCody Robibero <cody@robibe.ro>2021-12-23 19:38:10 -0700
commita04ab6b87637fe378759aaf2b7fa71726150b2b1 (patch)
tree62f4e5bdb272e9312bab469cbcda1e13591e7834 /Jellyfin.Server.Implementations/Users
parentc52a2f2f7b130d73a96cdac00f1e63531a04139b (diff)
parent8c7dd0a691d150ac4fa5719853554ff569abf1bb (diff)
Merge branch 'master' into studios-images-plugin
# Conflicts: # MediaBrowser.Providers/MediaBrowser.Providers.csproj
Diffstat (limited to 'Jellyfin.Server.Implementations/Users')
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs35
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs19
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs22
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs110
5 files changed, 81 insertions, 110 deletions
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 6a78e7ee6f..7480a05c25 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -1,9 +1,6 @@
using System;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Cryptography;
@@ -61,35 +58,25 @@ namespace Jellyfin.Server.Implementations.Users
}
// Handle the case when the stored password is null, but the user tried to login with a password
- if (resolvedUser.Password != null)
+ if (resolvedUser.Password == null)
{
- byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
-
- PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
- if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
- || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
- {
- byte[] calculatedHash = _cryptographyProvider.ComputeHash(
- readyHash.Id,
- passwordBytes,
- readyHash.Salt.ToArray());
-
- if (readyHash.Hash.SequenceEqual(calculatedHash))
- {
- success = true;
- }
- }
- else
- {
- throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}");
- }
+ throw new AuthenticationException("Invalid username or password");
}
+ PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
+ success = _cryptographyProvider.Verify(readyHash, password);
+
if (!success)
{
throw new AuthenticationException("Invalid username or password");
}
+ // Migrate old hashes to the new default
+ if (!string.Equals(readyHash.Id, _cryptographyProvider.DefaultHashMethod, StringComparison.Ordinal))
+ {
+ ChangePassword(resolvedUser, password);
+ }
+
return Task.FromResult(new ProviderAuthenticationResult
{
Username = username
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 9cc1c3e5e5..5e84255f91 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
- await using (var str = File.OpenRead(resetFile))
+ await using (var str = AsyncFile.OpenRead(resetFile))
{
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -66,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal),
- StringComparison.InvariantCultureIgnoreCase))
+ StringComparison.OrdinalIgnoreCase))
{
var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
@@ -92,13 +93,9 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
- string pin;
- using (var cryptoRandom = RandomNumberGenerator.Create())
- {
- byte[] bytes = new byte[4];
- cryptoRandom.GetBytes(bytes);
- pin = BitConverter.ToString(bytes);
- }
+ byte[] bytes = new byte[4];
+ RandomNumberGenerator.Fill(bytes);
+ string pin = BitConverter.ToString(bytes);
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json";
@@ -110,10 +107,9 @@ namespace Jellyfin.Server.Implementations.Users
UserName = user.Username
};
- await using (FileStream fileStream = File.OpenWrite(filePath))
+ await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
- await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
@@ -122,6 +118,7 @@ namespace Jellyfin.Server.Implementations.Users
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
+ PinFile = filePath
};
}
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
index dbba80c210..a471ea1d50 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
@@ -4,10 +4,10 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
namespace Jellyfin.Server.Implementations.Users
@@ -15,14 +15,12 @@ namespace Jellyfin.Server.Implementations.Users
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
- private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
- public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+ public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
{
_userManager = userManager;
- _authRepo = authRepo;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
}
@@ -38,27 +36,27 @@ namespace Jellyfin.Server.Implementations.Users
{
}
- private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+ private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
{
- UpdateDeviceAccess(user);
+ await UpdateDeviceAccess(user).ConfigureAwait(false);
}
}
- private void UpdateDeviceAccess(User user)
+ private async Task UpdateDeviceAccess(User user)
{
- var existing = _authRepo.Get(new AuthenticationInfoQuery
+ var existing = (await _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
- }).Items;
+ }).ConfigureAwait(false)).Items;
- foreach (var authInfo in existing)
+ foreach (var device in existing)
{
- if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+ if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
{
- _sessionManager.Logout(authInfo);
+ await _sessionManager.Logout(device).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index c8a589cabc..c89e3c74d5 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CA1307
+#pragma warning disable CA1309
using System;
using System.Collections.Generic;
@@ -35,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users
if (prefs == null)
{
- prefs = new DisplayPreferences(userId, itemId, client);
+ prefs = new DisplayPreferences(userId, itemId, client);
_dbContext.DisplayPreferences.Add(prefs);
}
@@ -67,7 +68,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc />
- public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
+ public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
.AsQueryable()
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index d1de5408c4..3d0a51ff67 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -5,7 +5,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -13,7 +12,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Events.Users;
using MediaBrowser.Common;
-using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@@ -137,17 +135,20 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentNullException(nameof(user));
}
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentException("Invalid username", nameof(newName));
- }
+ ThrowIfInvalidUsername(newName);
if (user.Username.Equals(newName, StringComparison.Ordinal))
{
throw new ArgumentException("The new and old names must be different.");
}
- if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
+ await using var dbContext = _dbProvider.CreateContext();
+
+ if (await dbContext.Users
+ .AsQueryable()
+ .Where(u => u.Username == newName && u.Id != user.Id)
+ .AnyAsync()
+ .ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
@@ -161,15 +162,6 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void UpdateUser(User user)
- {
- using var dbContext = _dbProvider.CreateContext();
- dbContext.Users.Update(user);
- _users[user.Id] = user;
- dbContext.SaveChanges();
- }
-
- /// <inheritdoc/>
public async Task UpdateUserAsync(User user)
{
await using var dbContext = _dbProvider.CreateContext();
@@ -187,12 +179,15 @@ namespace Jellyfin.Server.Implementations.Users
var user = new User(
name,
- _defaultAuthenticationProvider.GetType().FullName,
- _defaultPasswordResetProvider.GetType().FullName)
+ _defaultAuthenticationProvider.GetType().FullName!,
+ _defaultPasswordResetProvider.GetType().FullName!)
{
InternalId = max + 1
};
+ user.AddDefaultPermissions();
+ user.AddDefaultPreferences();
+
_users.Add(user.Id, user);
return user;
@@ -201,9 +196,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task<User> CreateUserAsync(string name)
{
- if (!IsValidUsername(name))
+ ThrowIfInvalidUsername(name);
+
+ if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
- throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "A user with the name '{0}' already exists.",
+ name));
}
await using var dbContext = _dbProvider.CreateContext();
@@ -246,16 +246,6 @@ namespace Jellyfin.Server.Implementations.Users
}
await using var dbContext = _dbProvider.CreateContext();
-
- // Clear all entities related to the user from the database.
- if (user.ProfileImage != null)
- {
- dbContext.Remove(user.ProfileImage);
- }
-
- dbContext.RemoveRange(user.Permissions);
- dbContext.RemoveRange(user.Preferences);
- dbContext.RemoveRange(user.AccessSchedules);
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
_users.Remove(userId);
@@ -270,9 +260,9 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ResetEasyPassword(User user)
+ public Task ResetEasyPassword(User user)
{
- ChangeEasyPassword(user, string.Empty, null);
+ return ChangeEasyPassword(user, string.Empty, null);
}
/// <inheritdoc/>
@@ -290,7 +280,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
+ public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
if (newPassword != null)
{
@@ -303,7 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
}
user.EasyPassword = newPasswordSha1;
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
_eventManager.Publish(new UserPasswordChangedEventArgs(user));
}
@@ -402,27 +392,18 @@ namespace Jellyfin.Server.Implementations.Users
}
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
- bool success;
- IAuthenticationProvider? authenticationProvider;
+ var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+ .ConfigureAwait(false);
+ var authenticationProvider = authResult.authenticationProvider;
+ var success = authResult.success;
- if (user != null)
- {
- var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
- .ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- success = authResult.success;
- }
- else
+ if (user == null)
{
- var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint)
- .ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
string updatedUsername = authResult.username;
- success = authResult.success;
if (success
&& authenticationProvider != null
- && !(authenticationProvider is DefaultAuthenticationProvider))
+ && authenticationProvider is not DefaultAuthenticationProvider)
{
// Trust the username returned by the authentication provider
username = updatedUsername;
@@ -442,7 +423,7 @@ namespace Jellyfin.Server.Implementations.Users
{
var providerId = authenticationProvider.GetType().FullName;
- if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+ if (providerId != null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
{
user.AuthenticationProviderId = providerId;
await UpdateUserAsync(user).ConfigureAwait(false);
@@ -547,11 +528,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- return new PinRedeemResult
- {
- Success = false,
- UsersReset = Array.Empty<string>()
- };
+ return new PinRedeemResult();
}
/// <inheritdoc />
@@ -718,6 +695,11 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task ClearProfileImageAsync(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return;
+ }
+
await using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
@@ -725,12 +707,22 @@ namespace Jellyfin.Server.Implementations.Users
_users[user.Id] = user;
}
+ internal static void ThrowIfInvalidUsername(string name)
+ {
+ if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name))
+ {
+ return;
+ }
+
+ throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
+ }
+
private static bool IsValidUsername(string name)
{
// 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 ('), periods (.) and spaces ( )
- return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
+ return Regex.IsMatch(name, @"^[\w\ \-'._@]+$");
}
private IAuthenticationProvider GetAuthenticationProvider(User user)
@@ -824,11 +816,7 @@ namespace Jellyfin.Server.Implementations.Users
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
- var hash = _cryptoProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(password),
- passwordHash.Salt.ToArray());
- success = passwordHash.Hash.SequenceEqual(hash);
+ success = _cryptoProvider.Verify(passwordHash, password);
}
return (authenticationProvider, username, success);