jellyfin/Jellyfin.Server.Implementations/Users/UserManager.cs

901 lines
40 KiB
C#
Raw Normal View History

#pragma warning disable CA1307
2020-05-13 04:10:35 +02:00
using System;
2020-10-27 01:31:10 +01:00
using System.Collections.Concurrent;
2020-05-13 04:10:35 +02:00
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
2020-05-15 23:24:01 +02:00
using System.Text.RegularExpressions;
2020-05-13 04:10:35 +02:00
using System.Threading.Tasks;
2020-05-15 23:24:01 +02:00
using Jellyfin.Data.Entities;
2020-05-13 04:10:35 +02:00
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
2020-08-15 21:55:15 +02:00
using Jellyfin.Data.Events.Users;
2020-05-19 21:58:25 +02:00
using MediaBrowser.Common;
2020-06-25 02:36:58 +02:00
using MediaBrowser.Common.Extensions;
2020-05-13 04:10:35 +02:00
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
2020-05-19 21:58:25 +02:00
using MediaBrowser.Controller.Drawing;
2020-08-15 21:55:15 +02:00
using MediaBrowser.Controller.Events;
2020-05-13 04:10:35 +02:00
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Users;
2020-07-12 20:45:52 +02:00
using Microsoft.EntityFrameworkCore;
2020-05-13 04:10:35 +02:00
using Microsoft.Extensions.Logging;
2020-05-15 23:24:01 +02:00
namespace Jellyfin.Server.Implementations.Users
2020-05-13 04:10:35 +02:00
{
2020-05-20 01:44:55 +02:00
/// <summary>
/// Manages the creation and retrieval of <see cref="User"/> instances.
/// </summary>
2020-05-13 04:10:35 +02:00
public class UserManager : IUserManager
{
2023-01-16 18:14:44 +01:00
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
2020-08-15 21:55:15 +02:00
private readonly IEventManager _eventManager;
2020-05-13 04:10:35 +02:00
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
2020-05-19 21:58:25 +02:00
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly ILogger<UserManager> _logger;
2020-06-17 16:24:25 +02:00
private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
2020-05-13 04:10:35 +02:00
2020-10-27 17:12:08 +01:00
private readonly IDictionary<Guid, User> _users;
2020-10-27 01:31:10 +01:00
2020-05-20 01:44:55 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
/// <param name="dbProvider">The database provider.</param>
2020-08-15 21:55:15 +02:00
/// <param name="eventManager">The event manager.</param>
2020-05-20 01:44:55 +02:00
/// <param name="cryptoProvider">The cryptography provider.</param>
/// <param name="networkManager">The network manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
2020-05-13 04:10:35 +02:00
public UserManager(
2023-01-16 18:14:44 +01:00
IDbContextFactory<JellyfinDbContext> dbProvider,
2020-08-15 21:55:15 +02:00
IEventManager eventManager,
2020-05-13 04:10:35 +02:00
ICryptoProvider cryptoProvider,
INetworkManager networkManager,
2020-05-19 21:58:25 +02:00
IApplicationHost appHost,
IImageProcessor imageProcessor,
ILogger<UserManager> logger)
2020-05-13 04:10:35 +02:00
{
_dbProvider = dbProvider;
2020-08-15 21:55:15 +02:00
_eventManager = eventManager;
2020-05-13 04:10:35 +02:00
_cryptoProvider = cryptoProvider;
_networkManager = networkManager;
2020-05-19 21:58:25 +02:00
_appHost = appHost;
_imageProcessor = imageProcessor;
2020-05-13 04:10:35 +02:00
_logger = logger;
2020-06-17 16:24:25 +02:00
2020-06-20 23:58:09 +02:00
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
2020-06-17 16:24:25 +02:00
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
2020-10-27 01:31:10 +01:00
2020-10-27 17:12:08 +01:00
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
2020-10-27 01:31:10 +01:00
foreach (var user in dbContext.Users
.AsSplitQuery()
2020-10-27 01:31:10 +01:00
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
2020-05-13 04:10:35 +02:00
}
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
2020-05-13 04:10:35 +02:00
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-10-27 17:12:08 +01:00
public IEnumerable<User> Users => _users.Values;
2020-05-13 04:10:35 +02:00
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-10-27 17:12:08 +01:00
public IEnumerable<Guid> UsersIds => _users.Keys;
2020-05-13 04:10:35 +02:00
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public User? GetUserById(Guid id)
2020-05-13 04:10:35 +02:00
{
if (id.Equals(default))
2020-05-13 04:10:35 +02:00
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
2020-10-27 01:31:10 +01:00
_users.TryGetValue(id, out var user);
return user;
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public User? GetUserByName(string name)
2020-05-13 04:10:35 +02:00
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Invalid username", nameof(name));
}
2020-10-27 01:31:10 +01:00
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-15 23:24:01 +02:00
public async Task RenameUser(User user, string newName)
2020-05-13 04:10:35 +02:00
{
ArgumentNullException.ThrowIfNull(user);
2020-05-13 04:10:35 +02:00
ThrowIfInvalidUsername(newName);
2020-05-13 04:10:35 +02:00
2020-07-20 04:21:30 +02:00
if (user.Username.Equals(newName, StringComparison.Ordinal))
2020-05-13 04:10:35 +02:00
{
throw new ArgumentException("The new and old names must be different.");
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
2020-05-13 04:10:35 +02:00
{
if (await dbContext.Users
.AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
.ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"A user with the name '{0}' already exists.",
newName));
}
user.Username = newName;
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
var eventArgs = new UserUpdatedEventArgs(user);
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
OnUserUpdated?.Invoke(this, eventArgs);
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-15 23:24:01 +02:00
public async Task UpdateUserAsync(User user)
2020-05-13 04:10:35 +02:00
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
}
2020-05-13 04:10:35 +02:00
}
2023-01-16 18:14:44 +01:00
internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
2020-07-22 20:57:29 +02:00
{
// TODO: Remove after user item data is migrated.
2020-10-06 04:51:52 +02:00
var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
2020-07-22 20:57:29 +02:00
: 0;
2020-10-27 01:31:10 +01:00
var user = new User(
2020-07-22 20:57:29 +02:00
name,
_defaultAuthenticationProvider.GetType().FullName!,
_defaultPasswordResetProvider.GetType().FullName!)
2020-07-22 20:57:29 +02:00
{
InternalId = max + 1
};
2020-10-27 01:31:10 +01:00
2021-03-17 22:42:45 +01:00
user.AddDefaultPermissions();
user.AddDefaultPreferences();
2020-10-27 01:31:10 +01:00
_users.Add(user.Id, user);
return user;
2020-07-22 20:57:29 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-07-22 20:57:29 +02:00
public async Task<User> CreateUserAsync(string name)
2020-05-13 04:10:35 +02:00
{
ThrowIfInvalidUsername(name);
2020-05-15 23:24:01 +02:00
if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"A user with the name '{0}' already exists.",
name));
}
User newUser;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
2020-05-20 19:49:44 +02:00
dbContext.Users.Add(newUser);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-05-13 04:10:35 +02:00
2020-08-15 21:55:15 +02:00
await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
2020-05-15 23:24:01 +02:00
2020-05-13 04:10:35 +02:00
return newUser;
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-12-11 16:15:43 +01:00
public async Task DeleteUserAsync(Guid userId)
2020-05-13 04:10:35 +02:00
{
2020-10-27 01:31:10 +01:00
if (!_users.TryGetValue(userId, out var user))
2020-05-13 04:10:35 +02:00
{
2020-06-25 02:36:58 +02:00
throw new ResourceNotFoundException(nameof(userId));
2020-05-13 04:10:35 +02:00
}
2020-10-27 01:31:10 +01:00
if (_users.Count == 1)
2020-05-13 04:10:35 +02:00
{
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));
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 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),
2020-06-25 02:19:47 +02:00
nameof(userId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-10-27 01:31:10 +01:00
_users.Remove(userId);
2020-08-15 21:55:15 +02:00
2020-12-11 16:15:43 +01:00
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-15 23:24:01 +02:00
public Task ResetPassword(User user)
2020-05-13 04:10:35 +02:00
{
return ChangePassword(user, string.Empty);
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2021-04-10 22:59:41 +02:00
public Task ResetEasyPassword(User user)
2020-05-13 04:10:35 +02:00
{
2021-04-10 22:59:41 +02:00
return ChangeEasyPassword(user, string.Empty, null);
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-15 23:24:01 +02:00
public async Task ChangePassword(User user, string newPassword)
2020-05-13 04:10:35 +02:00
{
ArgumentNullException.ThrowIfNull(user);
2020-05-13 04:10:35 +02:00
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
2020-08-15 21:55:15 +02:00
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2021-04-10 22:59:41 +02:00
public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
2020-05-13 04:10:35 +02:00
{
2022-12-05 15:01:13 +01:00
if (newPassword is not null)
{
newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString();
}
if (string.IsNullOrWhiteSpace(newPasswordSha1))
{
throw new ArgumentNullException(nameof(newPasswordSha1));
}
user.EasyPassword = newPasswordSha1;
2021-04-10 23:11:59 +02:00
await UpdateUserAsync(user).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
2020-05-13 04:10:35 +02:00
{
2020-06-08 05:11:51 +02:00
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
2020-05-13 04:10:35 +02:00
return new UserDto
{
2020-05-19 21:58:25 +02:00
Name = user.Username,
2020-05-13 04:10:35 +02:00
Id = user.Id,
2020-05-19 21:58:25 +02:00
ServerId = _appHost.SystemId,
2020-06-08 05:11:51 +02:00
HasPassword = hasPassword,
HasConfiguredPassword = hasPassword,
2020-06-08 02:16:51 +02:00
HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword),
2020-05-13 04:10:35 +02:00
EnableAutoLogin = user.EnableAutoLogin,
LastLoginDate = user.LastLoginDate,
LastActivityDate = user.LastActivityDate,
2022-12-05 15:01:13 +01:00
PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
2020-05-13 04:10:35 +02:00
Configuration = new UserConfiguration
{
SubtitleMode = user.SubtitleMode,
HidePlayedInLatest = user.HidePlayedInLatest,
EnableLocalPassword = user.EnableLocalPassword,
PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
DisplayCollectionsView = user.DisplayCollectionsView,
DisplayMissingEpisodes = user.DisplayMissingEpisodes,
AudioLanguagePreference = user.AudioLanguagePreference,
RememberAudioSelections = user.RememberAudioSelections,
EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = user.RememberSubtitleSelections,
2020-05-19 21:58:25 +02:00
SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
2020-05-13 04:10:35 +02:00
},
Policy = new UserPolicy
{
MaxParentalRating = user.MaxParentalAgeRating,
EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
2020-06-08 01:37:47 +02:00
RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
2020-05-15 23:24:01 +02:00
AuthenticationProviderId = user.AuthenticationProviderId,
2020-05-13 04:10:35 +02:00
PasswordResetProviderId = user.PasswordResetProviderId,
InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
MaxActiveSessions = user.MaxActiveSessions,
2020-05-13 04:10:35 +02:00
IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
IsHidden = user.HasPermission(PermissionKind.IsHidden),
IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers),
EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
2020-05-13 04:10:35 +02:00
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
2020-12-13 16:15:26 +01:00
EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
2020-05-13 04:10:35 +02:00
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
2020-12-13 16:15:26 +01:00
EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
2020-05-27 02:52:05 +02:00
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
2020-06-14 00:26:46 +02:00
SyncPlayAccess = user.SyncPlayAccess,
2020-12-13 16:15:26 +01:00
BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
2020-05-13 04:10:35 +02:00
}
};
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public async Task<User?> AuthenticateUser(
2020-05-13 04:10:35 +02:00
string username,
string password,
string passwordSha1,
string remoteEndPoint,
bool isUserSession)
{
if (string.IsNullOrWhiteSpace(username))
{
_logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
throw new ArgumentNullException(nameof(username));
}
2020-07-12 20:45:52 +02:00
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
2021-02-06 21:59:27 +01:00
var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
.ConfigureAwait(false);
2021-12-24 22:18:24 +01:00
var authenticationProvider = authResult.AuthenticationProvider;
var success = authResult.Success;
2020-05-13 04:10:35 +02:00
2022-12-05 15:00:20 +01:00
if (user is null)
2020-05-13 04:10:35 +02:00
{
2021-12-24 22:18:24 +01:00
string updatedUsername = authResult.Username;
2020-05-13 04:10:35 +02:00
if (success
2022-12-05 15:01:13 +01:00
&& authenticationProvider is not null
2021-02-06 21:59:27 +01:00
&& authenticationProvider is not DefaultAuthenticationProvider)
2020-05-13 04:10:35 +02:00
{
// Trust the username returned by the authentication provider
username = updatedUsername;
// Search the database for the user again
// the authentication provider might have created it
2020-07-12 20:45:52 +02:00
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
2020-05-13 04:10:35 +02:00
2022-12-05 15:01:13 +01:00
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
2020-05-13 04:10:35 +02:00
{
await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
}
}
2022-12-05 15:01:13 +01:00
if (success && user is not null && authenticationProvider is not null)
2020-05-13 04:10:35 +02:00
{
var providerId = authenticationProvider.GetType().FullName;
2022-12-05 15:01:13 +01:00
if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
2020-05-13 04:10:35 +02:00
{
user.AuthenticationProviderId = providerId;
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
2022-12-05 15:00:20 +01:00
if (user is null)
2020-05-13 04:10:35 +02:00
{
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
username,
remoteEndPoint);
throw new AuthenticationException("Invalid username or password entered.");
}
if (user.HasPermission(PermissionKind.IsDisabled))
{
_logger.LogInformation(
"Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException(
$"The {user.Username} account is currently disabled. Please consult with your administrator.");
}
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
!_networkManager.IsInLocalNetwork(remoteEndPoint))
{
_logger.LogInformation(
"Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException("Forbidden.");
}
if (!user.IsParentalScheduleAllowed())
{
_logger.LogInformation(
"Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException("User is not allowed access at this time.");
}
// Update LastActivityDate and LastLoginDate, then save
if (success)
{
if (isUserSession)
{
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
}
2020-05-15 23:24:01 +02:00
user.InvalidLoginAttemptCount = 0;
await UpdateUserAsync(user).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
}
else
{
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username,
remoteEndPoint);
}
return success ? user : null;
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
2022-12-05 15:01:13 +01:00
if (user is not null && isInNetwork)
2020-05-13 04:10:35 +02:00
{
var passwordResetProvider = GetPasswordResetProvider(user);
2020-06-20 23:58:09 +02:00
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, isInNetwork)
.ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
return result;
2020-05-13 04:10:35 +02:00
}
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.InNetworkRequired,
2020-05-13 04:10:35 +02:00
PinFile = string.Empty
};
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
foreach (var provider in _passwordResetProviders)
{
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
if (result.Success)
{
return result;
}
}
2021-10-26 13:56:30 +02:00
return new PinRedeemResult();
2020-05-13 04:10:35 +02:00
}
2020-06-09 20:01:21 +02:00
/// <inheritdoc />
2020-07-22 20:57:29 +02:00
public async Task InitializeAsync()
2020-06-09 20:01:21 +02:00
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
2020-10-27 01:31:10 +01:00
if (_users.Any())
2020-06-09 20:01:21 +02:00
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
2020-06-09 20:01:21 +02:00
{
defaultName = "MyJellyfinUser";
}
_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);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
2020-06-09 20:01:21 +02:00
dbContext.Users.Add(newUser);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-06-09 20:01:21 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
public NameIdPair[] GetAuthenticationProviders()
{
return _authenticationProviders
.Where(provider => provider.IsEnabled)
.OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
.ThenBy(i => i.Name)
.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.GetType().FullName
})
.ToArray();
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
public NameIdPair[] GetPasswordResetProviders()
{
return _passwordResetProviders
.Where(provider => provider.IsEnabled)
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
.ThenBy(i => i.Name)
.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.GetType().FullName
})
.ToArray();
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
2020-05-13 04:10:35 +02:00
{
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))
?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode;
user.HidePlayedInLatest = config.HidePlayedInLatest;
user.EnableLocalPassword = config.EnableLocalPassword;
user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
user.DisplayCollectionsView = config.DisplayCollectionsView;
user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
user.AudioLanguagePreference = config.AudioLanguagePreference;
user.RememberAudioSelections = config.RememberAudioSelections;
user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
2020-05-13 04:10:35 +02:00
{
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))
?? 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
{
-1 => null,
0 => 3,
_ => policy.LoginAttemptsBeforeLockout
};
2020-05-13 04:10:35 +02:00
user.MaxParentalAgeRating = policy.MaxParentalRating;
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
user.AuthenticationProviderId = policy.AuthenticationProviderId;
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
user.MaxActiveSessions = policy.MaxActiveSessions;
user.SyncPlayAccess = policy.SyncPlayAccess;
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
user.AccessSchedules.Clear();
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
user.AccessSchedules.Add(policyAccessSchedule);
}
// TODO: fix this at some point
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-05-13 04:10:35 +02:00
}
2020-06-12 00:28:49 +02:00
/// <inheritdoc/>
public async Task ClearProfileImageAsync(User user)
2020-06-11 23:51:02 +02:00
{
2022-12-05 15:00:20 +01:00
if (user.ProfileImage is null)
2021-10-04 15:43:40 +02:00
{
return;
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-10-27 17:28:37 +01:00
user.ProfileImage = null;
2020-11-15 17:30:04 +01:00
_users[user.Id] = user;
2020-06-11 23:51:02 +02:00
}
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));
}
2023-02-17 15:00:06 +01:00
private static bool IsValidUsername(ReadOnlySpan<char> name)
2020-05-13 04:10:35 +02:00
{
2020-05-15 23:24:01 +02:00
// 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
2020-07-26 13:57:22 +02:00
// 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\ \-'._@]+$");
2020-05-13 04:10:35 +02:00
}
2020-05-15 23:24:01 +02:00
private IAuthenticationProvider GetAuthenticationProvider(User user)
2020-05-13 04:10:35 +02:00
{
return GetAuthenticationProviders(user)[0];
}
2020-05-15 23:24:01 +02:00
private IPasswordResetProvider GetPasswordResetProvider(User user)
2020-05-13 04:10:35 +02:00
{
return GetPasswordResetProviders(user)[0];
}
2020-06-09 18:21:21 +02:00
private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user)
2020-05-13 04:10:35 +02:00
{
var authenticationProviderId = user?.AuthenticationProviderId;
var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
if (!string.IsNullOrEmpty(authenticationProviderId))
{
providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (providers.Count == 0)
{
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
_logger.LogWarning(
2020-05-15 23:24:01 +02:00
"User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
2020-05-13 04:10:35 +02:00
user?.Username,
user?.AuthenticationProviderId);
providers = new List<IAuthenticationProvider>
{
_invalidAuthProvider
};
}
return providers;
}
2020-05-15 23:24:01 +02:00
private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
2020-05-13 04:10:35 +02:00
{
var passwordResetProviderId = user.PasswordResetProviderId;
2020-05-13 04:10:35 +02:00
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
if (!string.IsNullOrEmpty(passwordResetProviderId))
{
providers = providers.Where(i =>
string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
if (providers.Length == 0)
{
providers = new IPasswordResetProvider[]
{
_defaultPasswordResetProvider
};
}
return providers;
}
2021-12-24 22:18:24 +01:00
private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
2020-05-13 04:10:35 +02:00
string username,
string password,
2020-06-09 18:21:21 +02:00
User? user,
2020-05-13 04:10:35 +02:00
string remoteEndPoint)
{
bool success = false;
2020-06-09 18:21:21 +02:00
IAuthenticationProvider? authenticationProvider = null;
2020-05-13 04:10:35 +02:00
foreach (var provider in GetAuthenticationProviders(user))
{
var providerAuthResult =
await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
2021-12-24 22:18:24 +01:00
var updatedUsername = providerAuthResult.Username;
success = providerAuthResult.Success;
2020-05-13 04:10:35 +02:00
if (success)
{
authenticationProvider = provider;
username = updatedUsername;
break;
}
}
if (!success
&& _networkManager.IsInLocalNetwork(remoteEndPoint)
&& user?.EnableLocalPassword == true
&& !string.IsNullOrEmpty(user.EasyPassword))
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
success = _cryptoProvider.Verify(passwordHash, password);
2020-05-13 04:10:35 +02:00
}
return (authenticationProvider, username, success);
}
2021-12-24 22:18:24 +01:00
private async Task<(string Username, bool Success)> AuthenticateWithProvider(
2020-05-13 04:10:35 +02:00
IAuthenticationProvider provider,
string username,
string password,
2020-06-09 18:21:21 +02:00
User? resolvedUser)
2020-05-13 04:10:35 +02:00
{
try
{
var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
: await provider.Authenticate(username, password).ConfigureAwait(false);
if (authenticationResult.Username != username)
{
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
username = authenticationResult.Username;
}
return (username, true);
}
catch (AuthenticationException ex)
{
_logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
return (username, false);
}
}
private async Task IncrementInvalidLoginAttemptCount(User user)
2020-05-13 04:10:35 +02:00
{
user.InvalidLoginAttemptCount++;
2020-05-13 04:10:35 +02:00
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
2020-05-13 04:10:35 +02:00
{
user.SetPermission(PermissionKind.IsDisabled, true);
2020-08-15 21:55:15 +02:00
await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
_logger.LogWarning(
2020-05-15 23:24:01 +02:00
"Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
2020-05-13 04:10:35 +02:00
user.Username,
user.InvalidLoginAttemptCount);
2020-05-13 04:10:35 +02:00
}
await UpdateUserAsync(user).ConfigureAwait(false);
2020-05-13 04:10:35 +02:00
}
2023-01-16 18:14:44 +01:00
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-05-13 04:10:35 +02:00
}
}