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

887 lines
38 KiB
C#
Raw Normal View History

2020-06-09 18:21:21 +02:00
#nullable enable
#pragma warning disable CA1307
2020-05-13 04:10:35 +02:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
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;
2020-05-19 21:58:25 +02:00
using MediaBrowser.Common;
2020-05-13 04:10:35 +02:00
using MediaBrowser.Common.Cryptography;
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-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.Events;
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
{
private readonly JellyfinDbProvider _dbProvider;
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-05-13 04:10:35 +02:00
2020-06-09 18:21:21 +02:00
private IAuthenticationProvider[] _authenticationProviders = null!;
private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
private InvalidAuthProvider _invalidAuthProvider = null!;
private IPasswordResetProvider[] _passwordResetProviders = null!;
private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
2020-05-13 04:10:35 +02: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>
/// <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(
JellyfinDbProvider dbProvider,
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;
_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-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public event EventHandler<GenericEventArgs<User>>? OnUserPasswordChanged;
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
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public event EventHandler<GenericEventArgs<User>>? OnUserCreated;
2020-05-13 04:10:35 +02:00
/// <inheritdoc/>
2020-06-09 18:21:21 +02:00
public event EventHandler<GenericEventArgs<User>>? OnUserDeleted;
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 event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
2020-05-13 04:10:35 +02:00
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-07-12 20:45:52 +02:00
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)
2020-07-13 20:09:20 +02:00
.AsEnumerable();
2020-07-12 20:45:52 +02:00
}
}
2020-05-13 04:10:35 +02:00
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-07 19:36:43 +02:00
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
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 == Guid.Empty)
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
2020-07-12 20:45:52 +02:00
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);
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-07-12 20:45:52 +02:00
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));
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
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(newName))
{
throw new ArgumentException("Invalid username", nameof(newName));
}
2020-07-12 20:45:52 +02:00
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
2020-05-13 04:10:35 +02:00
{
throw new ArgumentException("The new and old names must be different.");
}
2020-05-19 21:58:25 +02:00
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
2020-05-13 04:10:35 +02:00
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"A user with the name '{0}' already exists.",
newName));
}
user.Username = newName;
await UpdateUserAsync(user).ConfigureAwait(false);
2020-05-15 23:24:01 +02:00
OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
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 void UpdateUser(User user)
2020-05-13 04:10:35 +02:00
{
2020-07-12 20:45:52 +02:00
using var dbContext = _dbProvider.CreateContext();
2020-05-13 04:10:35 +02:00
dbContext.Users.Update(user);
dbContext.SaveChanges();
}
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
{
2020-07-12 20:45:52 +02:00
await using var dbContext = _dbProvider.CreateContext();
2020-05-13 04:10:35 +02:00
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-15 23:24:01 +02:00
public User CreateUser(string name)
2020-05-13 04:10:35 +02:00
{
2020-05-15 23:24:01 +02:00
if (!IsValidUsername(name))
{
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
2020-07-12 20:45:52 +02:00
using var dbContext = _dbProvider.CreateContext();
2020-05-13 04:10:35 +02:00
// TODO: Remove after user item data is migrated.
2020-06-11 02:14:24 +02:00
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
2020-05-20 19:49:44 +02:00
var newUser = new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
2020-05-20 19:49:44 +02:00
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
2020-05-13 04:10:35 +02:00
dbContext.Users.Add(newUser);
dbContext.SaveChanges();
2020-05-15 23:24:01 +02:00
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
2020-05-13 04:10:35 +02:00
return newUser;
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-06-25 02:19:47 +02:00
public void DeleteUser(Guid userId)
2020-05-13 04:10:35 +02:00
{
2020-07-12 20:45:52 +02:00
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);
2020-05-13 04:10:35 +02:00
if (user == null)
{
2020-06-25 02:36:58 +02:00
throw new ResourceNotFoundException(nameof(userId));
2020-05-13 04:10:35 +02:00
}
if (dbContext.Users.Find(user.Id) == null)
2020-05-13 04:10:35 +02:00
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
user.Username,
user.Id));
}
if (dbContext.Users.Count() == 1)
{
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));
}
// Clear all entities related to the user from the database.
if (user.ProfileImage != null)
{
dbContext.Remove(user.ProfileImage);
2020-05-13 04:10:35 +02:00
}
2020-06-25 02:19:47 +02:00
dbContext.RemoveRange(user.Permissions);
dbContext.RemoveRange(user.Preferences);
dbContext.RemoveRange(user.AccessSchedules);
2020-05-13 04:10:35 +02:00
dbContext.Users.Remove(user);
dbContext.SaveChanges();
2020-05-15 23:24:01 +02:00
OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
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/>
2020-05-15 23:24:01 +02:00
public void ResetEasyPassword(User user)
2020-05-13 04:10:35 +02:00
{
ChangeEasyPassword(user, string.Empty, null);
}
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
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
2020-05-15 23:24:01 +02:00
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(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 void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
2020-05-13 04:10:35 +02:00
{
if (newPassword != null)
{
newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString();
}
if (string.IsNullOrWhiteSpace(newPasswordSha1))
{
throw new ArgumentNullException(nameof(newPasswordSha1));
}
user.EasyPassword = newPasswordSha1;
2020-05-13 04:10:35 +02:00
UpdateUser(user);
2020-05-15 23:24:01 +02:00
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(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 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,
2020-05-19 21:58:25 +02:00
PrimaryImageTag = user.ProfileImage != 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,
2020-05-13 04:10:35 +02:00
OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
},
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,
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),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreference(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,
BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
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));
2020-05-13 04:10:35 +02:00
bool success;
2020-06-09 18:21:21 +02:00
IAuthenticationProvider? authenticationProvider;
2020-05-13 04:10:35 +02:00
if (user != null)
{
var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
.ConfigureAwait(false);
authenticationProvider = authResult.authenticationProvider;
success = authResult.success;
}
else
{
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))
{
// 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
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
{
UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}
if (success && user != null && authenticationProvider != null)
{
var providerId = authenticationProvider.GetType().FullName;
if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
{
user.AuthenticationProviderId = providerId;
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
if (user == null)
{
_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
{
IncrementInvalidLoginAttemptCount(user);
_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);
if (user != null && isInNetwork)
{
var passwordResetProvider = GetPasswordResetProvider(user);
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
}
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;
}
}
return new PinRedeemResult
{
Success = false,
UsersReset = Array.Empty<string>()
};
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
{
_authenticationProviders = authenticationProviders.ToArray();
_passwordResetProviders = passwordResetProviders.ToArray();
2020-05-15 23:24:01 +02:00
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
2020-05-13 04:10:35 +02:00
}
2020-06-09 20:01:21 +02:00
/// <inheritdoc />
public void Initialize()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
2020-07-12 20:45:52 +02:00
using var dbContext = _dbProvider.CreateContext();
2020-06-09 20:01:21 +02:00
if (dbContext.Users.Any())
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(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);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
2020-06-11 02:14:24 +02:00
dbContext.Users.Update(newUser);
2020-06-09 20:01:21 +02:00
dbContext.SaveChanges();
}
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/>
2020-05-13 04:10:35 +02:00
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!");
2020-05-13 04:10:35 +02:00
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);
dbContext.SaveChanges();
2020-05-13 04:10:35 +02:00
}
2020-05-20 01:44:55 +02:00
/// <inheritdoc/>
2020-05-13 04:10:35 +02:00
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!");
2020-06-14 01:30:45 +02:00
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
2020-05-23 22:07:42 +02:00
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
2020-05-23 20:53:24 +02:00
{
-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;
2020-05-15 23:24:01 +02:00
user.AuthenticationProviderId = policy.AuthenticationProviderId;
2020-05-13 04:10:35 +02:00
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
2020-05-23 22:07:42 +02:00
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
2020-05-27 02:52:05 +02:00
user.SyncPlayAccess = policy.SyncPlayAccess;
2020-05-13 04:10:35 +02:00
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.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
user.AccessSchedules.Clear();
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
user.AccessSchedules.Add(policyAccessSchedule);
}
2020-06-14 00:23:13 +02:00
// TODO: fix this at some point
user.SetPreference(
PreferenceKind.BlockUnratedItems,
policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
2020-05-13 04:10:35 +02:00
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
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);
dbContext.SaveChanges();
2020-05-13 04:10:35 +02:00
}
2020-06-12 00:28:49 +02:00
/// <inheritdoc/>
2020-06-11 23:51:02 +02:00
public void ClearProfileImage(User user)
{
2020-07-12 20:45:52 +02:00
using var dbContext = _dbProvider.CreateContext();
2020-06-13 22:38:17 +02:00
dbContext.Remove(user.ProfileImage);
dbContext.SaveChanges();
2020-06-11 23:51:02 +02:00
}
private static bool IsValidUsername(string 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
// 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\-'._@]*$");
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;
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;
}
2020-06-09 18:21:21 +02: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);
var updatedUsername = providerAuthResult.username;
success = providerAuthResult.success;
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);
var hash = _cryptoProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(password),
passwordHash.Salt.ToArray());
success = passwordHash.Hash.SequenceEqual(hash);
}
return (authenticationProvider, username, success);
}
private async Task<(string username, bool success)> AuthenticateWithProvider(
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);
}
}
2020-05-15 23:24:01 +02:00
private void 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-05-15 23:24:01 +02:00
OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
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
}
UpdateUser(user);
}
}
}