< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Users.UserManager
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Users/UserManager.cs
Line coverage
72%
Covered lines: 120
Uncovered lines: 46
Coverable lines: 166
Total lines: 897
Line coverage: 72.2%
Branch coverage
44%
Covered branches: 17
Total branches: 38
Branch coverage: 44.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%2295.65%
get_Users()100%11100%
get_UsersIds()100%210%
GetUserById(...)50%2275%
GetUserByName(...)50%2266.66%
ResetPassword(...)100%210%
GetUserDto(...)42.85%1414100%
GetAuthenticationProviders()100%210%
GetPasswordResetProviders()100%210%
ThrowIfInvalidUsername(...)100%44100%
GetAuthenticationProvider(...)100%11100%
GetPasswordResetProvider(...)100%210%
GetAuthenticationProviders(...)40%291042.85%
GetPasswordResetProviders(...)0%2040%

File(s)

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

#LineLine coverage
 1#pragma warning disable CA1307
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Linq;
 8using System.Text.RegularExpressions;
 9using System.Threading.Tasks;
 10using Jellyfin.Data;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Events.Users;
 14using Jellyfin.Database.Implementations;
 15using Jellyfin.Database.Implementations.Entities;
 16using Jellyfin.Database.Implementations.Enums;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Common.Net;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Drawing;
 24using MediaBrowser.Controller.Events;
 25using MediaBrowser.Controller.Library;
 26using MediaBrowser.Controller.Net;
 27using MediaBrowser.Model.Configuration;
 28using MediaBrowser.Model.Dto;
 29using MediaBrowser.Model.Users;
 30using Microsoft.EntityFrameworkCore;
 31using Microsoft.Extensions.Logging;
 32
 33namespace Jellyfin.Server.Implementations.Users
 34{
 35    /// <summary>
 36    /// Manages the creation and retrieval of <see cref="User"/> instances.
 37    /// </summary>
 38    public partial class UserManager : IUserManager
 39    {
 40        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 41        private readonly IEventManager _eventManager;
 42        private readonly INetworkManager _networkManager;
 43        private readonly IApplicationHost _appHost;
 44        private readonly IImageProcessor _imageProcessor;
 45        private readonly ILogger<UserManager> _logger;
 46        private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
 47        private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
 48        private readonly InvalidAuthProvider _invalidAuthProvider;
 49        private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
 50        private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
 51        private readonly IServerConfigurationManager _serverConfigurationManager;
 52
 53        private readonly IDictionary<Guid, User> _users;
 54
 55        /// <summary>
 56        /// Initializes a new instance of the <see cref="UserManager"/> class.
 57        /// </summary>
 58        /// <param name="dbProvider">The database provider.</param>
 59        /// <param name="eventManager">The event manager.</param>
 60        /// <param name="networkManager">The network manager.</param>
 61        /// <param name="appHost">The application host.</param>
 62        /// <param name="imageProcessor">The image processor.</param>
 63        /// <param name="logger">The logger.</param>
 64        /// <param name="serverConfigurationManager">The system config manager.</param>
 65        /// <param name="passwordResetProviders">The password reset providers.</param>
 66        /// <param name="authenticationProviders">The authentication providers.</param>
 67        public UserManager(
 68            IDbContextFactory<JellyfinDbContext> dbProvider,
 69            IEventManager eventManager,
 70            INetworkManager networkManager,
 71            IApplicationHost appHost,
 72            IImageProcessor imageProcessor,
 73            ILogger<UserManager> logger,
 74            IServerConfigurationManager serverConfigurationManager,
 75            IEnumerable<IPasswordResetProvider> passwordResetProviders,
 76            IEnumerable<IAuthenticationProvider> authenticationProviders)
 77        {
 2178            _dbProvider = dbProvider;
 2179            _eventManager = eventManager;
 2180            _networkManager = networkManager;
 2181            _appHost = appHost;
 2182            _imageProcessor = imageProcessor;
 2183            _logger = logger;
 2184            _serverConfigurationManager = serverConfigurationManager;
 85
 2186            _passwordResetProviders = passwordResetProviders.ToList();
 2187            _authenticationProviders = authenticationProviders.ToList();
 88
 2189            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 2190            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 2191            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 92
 2193            _users = new ConcurrentDictionary<Guid, User>();
 2194            using var dbContext = _dbProvider.CreateDbContext();
 4295            foreach (var user in dbContext.Users
 2196                .AsSplitQuery()
 2197                .Include(user => user.Permissions)
 2198                .Include(user => user.Preferences)
 2199                .Include(user => user.AccessSchedules)
 21100                .Include(user => user.ProfileImage)
 21101                .AsEnumerable())
 102            {
 0103                _users.Add(user.Id, user);
 104            }
 21105        }
 106
 107        /// <inheritdoc/>
 108        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 109
 110        /// <inheritdoc/>
 39111        public IEnumerable<User> Users => _users.Values;
 112
 113        /// <inheritdoc/>
 0114        public IEnumerable<Guid> UsersIds => _users.Keys;
 115
 116        // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
 117        // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
 118        // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes
 119        [GeneratedRegex(@"^(?!\s)[\w\ \-'._@+]+(?<!\s)$")]
 120        private static partial Regex ValidUsernameRegex();
 121
 122        /// <inheritdoc/>
 123        public User? GetUserById(Guid id)
 124        {
 247125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 247130            _users.TryGetValue(id, out var user);
 247131            return user;
 132        }
 133
 134        /// <inheritdoc/>
 135        public User? GetUserByName(string name)
 136        {
 15137            if (string.IsNullOrWhiteSpace(name))
 138            {
 0139                throw new ArgumentException("Invalid username", nameof(name));
 140            }
 141
 15142            return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)
 143        }
 144
 145        /// <inheritdoc/>
 146        public async Task RenameUser(User user, string newName)
 147        {
 148            ArgumentNullException.ThrowIfNull(user);
 149
 150            ThrowIfInvalidUsername(newName);
 151
 152            if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
 153            {
 154                throw new ArgumentException("The new and old names must be different.");
 155            }
 156
 157            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 158            await using (dbContext.ConfigureAwait(false))
 159            {
 160#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 161#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current 
 162#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale setti
 163                if (await dbContext.Users
 164                        .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && !u.Id.Equals(user.Id))
 165                        .ConfigureAwait(false))
 166                {
 167                    throw new ArgumentException(string.Format(
 168                        CultureInfo.InvariantCulture,
 169                        "A user with the name '{0}' already exists.",
 170                        newName));
 171                }
 172#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale setti
 173#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current 
 174#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 175
 176                user.Username = newName;
 177                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 178            }
 179
 180            var eventArgs = new UserUpdatedEventArgs(user);
 181            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 182            OnUserUpdated?.Invoke(this, eventArgs);
 183        }
 184
 185        /// <inheritdoc/>
 186        public async Task UpdateUserAsync(User user)
 187        {
 188            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 189            await using (dbContext.ConfigureAwait(false))
 190            {
 191                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 192            }
 193        }
 194
 195        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 196        {
 197            // TODO: Remove after user item data is migrated.
 198            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 199                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 200                : 0;
 201
 202            var user = new User(
 203                name,
 204                _defaultAuthenticationProvider.GetType().FullName!,
 205                _defaultPasswordResetProvider.GetType().FullName!)
 206            {
 207                InternalId = max + 1
 208            };
 209
 210            user.AddDefaultPermissions();
 211            user.AddDefaultPreferences();
 212
 213            return user;
 214        }
 215
 216        /// <inheritdoc/>
 217        public async Task<User> CreateUserAsync(string name)
 218        {
 219            ThrowIfInvalidUsername(name);
 220
 221            if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
 222            {
 223                throw new ArgumentException(string.Format(
 224                    CultureInfo.InvariantCulture,
 225                    "A user with the name '{0}' already exists.",
 226                    name));
 227            }
 228
 229            User newUser;
 230            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 231            await using (dbContext.ConfigureAwait(false))
 232            {
 233                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 234
 235                dbContext.Users.Add(newUser);
 236                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 237                _users.Add(newUser.Id, newUser);
 238            }
 239
 240            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 241
 242            return newUser;
 243        }
 244
 245        /// <inheritdoc/>
 246        public async Task DeleteUserAsync(Guid userId)
 247        {
 248            if (!_users.TryGetValue(userId, out var user))
 249            {
 250                throw new ResourceNotFoundException(nameof(userId));
 251            }
 252
 253            if (_users.Count == 1)
 254            {
 255                throw new InvalidOperationException(string.Format(
 256                    CultureInfo.InvariantCulture,
 257                    "The user '{0}' cannot be deleted because there must be at least one user in the system.",
 258                    user.Username));
 259            }
 260
 261            if (user.HasPermission(PermissionKind.IsAdministrator)
 262                && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
 263            {
 264                throw new ArgumentException(
 265                    string.Format(
 266                        CultureInfo.InvariantCulture,
 267                        "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
 268                        user.Username),
 269                    nameof(userId));
 270            }
 271
 272            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 273            await using (dbContext.ConfigureAwait(false))
 274            {
 275                dbContext.Users.Attach(user);
 276                dbContext.Users.Remove(user);
 277                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 278            }
 279
 280            _users.Remove(userId);
 281
 282            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 283        }
 284
 285        /// <inheritdoc/>
 286        public Task ResetPassword(User user)
 287        {
 0288            return ChangePassword(user, string.Empty);
 289        }
 290
 291        /// <inheritdoc/>
 292        public async Task ChangePassword(User user, string newPassword)
 293        {
 294            ArgumentNullException.ThrowIfNull(user);
 295            if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
 296            {
 297                throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
 298            }
 299
 300            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
 301            await UpdateUserAsync(user).ConfigureAwait(false);
 302
 303            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
 304        }
 305
 306        /// <inheritdoc/>
 307        public UserDto GetUserDto(User user, string? remoteEndPoint = null)
 308        {
 38309            var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
 38310            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 38311            return new UserDto
 38312            {
 38313                Name = user.Username,
 38314                Id = user.Id,
 38315                ServerId = _appHost.SystemId,
 38316                HasPassword = hasPassword,
 38317                HasConfiguredPassword = hasPassword,
 38318                EnableAutoLogin = user.EnableAutoLogin,
 38319                LastLoginDate = user.LastLoginDate,
 38320                LastActivityDate = user.LastActivityDate,
 38321                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 38322                Configuration = new UserConfiguration
 38323                {
 38324                    SubtitleMode = user.SubtitleMode,
 38325                    HidePlayedInLatest = user.HidePlayedInLatest,
 38326                    EnableLocalPassword = user.EnableLocalPassword,
 38327                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 38328                    DisplayCollectionsView = user.DisplayCollectionsView,
 38329                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 38330                    AudioLanguagePreference = user.AudioLanguagePreference,
 38331                    RememberAudioSelections = user.RememberAudioSelections,
 38332                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 38333                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 38334                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 38335                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 38336                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 38337                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 38338                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 38339                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 38340                        ? castReceiverApplications.FirstOrDefault()?.Id
 38341                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 38342                          ?? castReceiverApplications.FirstOrDefault()?.Id
 38343                },
 38344                Policy = new UserPolicy
 38345                {
 38346                    MaxParentalRating = user.MaxParentalRatingScore,
 38347                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
 38348                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 38349                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 38350                    AuthenticationProviderId = user.AuthenticationProviderId,
 38351                    PasswordResetProviderId = user.PasswordResetProviderId,
 38352                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 38353                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 38354                    MaxActiveSessions = user.MaxActiveSessions,
 38355                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 38356                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 38357                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 38358                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 38359                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 38360                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 38361                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 38362                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 38363                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 38364                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 38365                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 38366                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 38367                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 38368                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 38369                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 38370                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 38371                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 38372                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 38373                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 38374                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 38375                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 38376                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 38377                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 38378                    AccessSchedules = user.AccessSchedules.ToArray(),
 38379                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 38380                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 38381                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 38382                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 38383                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 38384                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 38385                    SyncPlayAccess = user.SyncPlayAccess,
 38386                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 38387                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 38388                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 38389                }
 38390            };
 391        }
 392
 393        /// <inheritdoc/>
 394        public async Task<User?> AuthenticateUser(
 395            string username,
 396            string password,
 397            string remoteEndPoint,
 398            bool isUserSession)
 399        {
 400            if (string.IsNullOrWhiteSpace(username))
 401            {
 402                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndP
 403                throw new ArgumentNullException(nameof(username));
 404            }
 405
 406            var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)
 407            var authResult = await AuthenticateLocalUser(username, password, user)
 408                .ConfigureAwait(false);
 409            var authenticationProvider = authResult.AuthenticationProvider;
 410            var success = authResult.Success;
 411
 412            if (user is null)
 413            {
 414                string updatedUsername = authResult.Username;
 415
 416                if (success
 417                    && authenticationProvider is not null
 418                    && authenticationProvider is not DefaultAuthenticationProvider)
 419                {
 420                    // Trust the username returned by the authentication provider
 421                    username = updatedUsername;
 422
 423                    // Search the database for the user again
 424                    // the authentication provider might have created it
 425                    user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreC
 426
 427                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
 428                    {
 429                        await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
 430                    }
 431                }
 432            }
 433
 434            if (success && user is not null && authenticationProvider is not null)
 435            {
 436                var providerId = authenticationProvider.GetType().FullName;
 437
 438                if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison
 439                {
 440                    user.AuthenticationProviderId = providerId;
 441                    await UpdateUserAsync(user).ConfigureAwait(false);
 442                }
 443            }
 444
 445            if (user is null)
 446            {
 447                _logger.LogInformation(
 448                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 449                    username,
 450                    remoteEndPoint);
 451                throw new AuthenticationException("Invalid username or password entered.");
 452            }
 453
 454            if (user.HasPermission(PermissionKind.IsDisabled))
 455            {
 456                _logger.LogInformation(
 457                    "Authentication request for {UserName} has been denied because this account is currently disabled (I
 458                    username,
 459                    remoteEndPoint);
 460                throw new SecurityException(
 461                    $"The {user.Username} account is currently disabled. Please consult with your administrator.");
 462            }
 463
 464            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 465                !_networkManager.IsInLocalNetwork(remoteEndPoint))
 466            {
 467                _logger.LogInformation(
 468                    "Authentication request for {UserName} forbidden: remote access disabled and user not in local netwo
 469                    username,
 470                    remoteEndPoint);
 471                throw new SecurityException("Forbidden.");
 472            }
 473
 474            if (!user.IsParentalScheduleAllowed())
 475            {
 476                _logger.LogInformation(
 477                    "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {I
 478                    username,
 479                    remoteEndPoint);
 480                throw new SecurityException("User is not allowed access at this time.");
 481            }
 482
 483            // Update LastActivityDate and LastLoginDate, then save
 484            if (success)
 485            {
 486                if (isUserSession)
 487                {
 488                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
 489                }
 490
 491                user.InvalidLoginAttemptCount = 0;
 492                await UpdateUserAsync(user).ConfigureAwait(false);
 493                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 494            }
 495            else
 496            {
 497                await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
 498                _logger.LogInformation(
 499                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 500                    user.Username,
 501                    remoteEndPoint);
 502            }
 503
 504            return success ? user : null;
 505        }
 506
 507        /// <inheritdoc/>
 508        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 509        {
 510            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 511
 512            if (user is not null && isInNetwork)
 513            {
 514                var passwordResetProvider = GetPasswordResetProvider(user);
 515                var result = await passwordResetProvider
 516                    .StartForgotPasswordProcess(user, isInNetwork)
 517                    .ConfigureAwait(false);
 518
 519                await UpdateUserAsync(user).ConfigureAwait(false);
 520                return result;
 521            }
 522
 523            return new ForgotPasswordResult
 524            {
 525                Action = ForgotPasswordAction.InNetworkRequired,
 526                PinFile = string.Empty
 527            };
 528        }
 529
 530        /// <inheritdoc/>
 531        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 532        {
 533            foreach (var provider in _passwordResetProviders)
 534            {
 535                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 536
 537                if (result.Success)
 538                {
 539                    return result;
 540                }
 541            }
 542
 543            return new PinRedeemResult();
 544        }
 545
 546        /// <inheritdoc />
 547        public async Task InitializeAsync()
 548        {
 549            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 550            if (_users.Any())
 551            {
 552                return;
 553            }
 554
 555            var defaultName = Environment.UserName;
 556            if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 557            {
 558                defaultName = "MyJellyfinUser";
 559            }
 560
 561            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 562
 563            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 564            await using (dbContext.ConfigureAwait(false))
 565            {
 566                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 567                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 568                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 569                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 570
 571                dbContext.Users.Add(newUser);
 572                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 573                _users.Add(newUser.Id, newUser);
 574            }
 575        }
 576
 577        /// <inheritdoc/>
 578        public NameIdPair[] GetAuthenticationProviders()
 579        {
 0580            return _authenticationProviders
 0581                .Where(provider => provider.IsEnabled)
 0582                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0583                .ThenBy(i => i.Name)
 0584                .Select(i => new NameIdPair
 0585                {
 0586                    Name = i.Name,
 0587                    Id = i.GetType().FullName
 0588                })
 0589                .ToArray();
 590        }
 591
 592        /// <inheritdoc/>
 593        public NameIdPair[] GetPasswordResetProviders()
 594        {
 0595            return _passwordResetProviders
 0596                .Where(provider => provider.IsEnabled)
 0597                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0598                .ThenBy(i => i.Name)
 0599                .Select(i => new NameIdPair
 0600                {
 0601                    Name = i.Name,
 0602                    Id = i.GetType().FullName
 0603                })
 0604                .ToArray();
 605        }
 606
 607        /// <inheritdoc/>
 608        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 609        {
 610            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 611            await using (dbContext.ConfigureAwait(false))
 612            {
 613                var user = dbContext.Users
 614                               .Include(u => u.Permissions)
 615                               .Include(u => u.Preferences)
 616                               .Include(u => u.AccessSchedules)
 617                               .Include(u => u.ProfileImage)
 618                               .FirstOrDefault(u => u.Id.Equals(userId))
 619                           ?? throw new ArgumentException("No user exists with given Id!");
 620
 621                user.SubtitleMode = config.SubtitleMode;
 622                user.HidePlayedInLatest = config.HidePlayedInLatest;
 623                user.EnableLocalPassword = config.EnableLocalPassword;
 624                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 625                user.DisplayCollectionsView = config.DisplayCollectionsView;
 626                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 627                user.AudioLanguagePreference = config.AudioLanguagePreference;
 628                user.RememberAudioSelections = config.RememberAudioSelections;
 629                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 630                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 631                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 632
 633                // Only set cast receiver id if it is passed in and it exists in the server config.
 634                if (!string.IsNullOrEmpty(config.CastReceiverId)
 635                    && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, c
 636                {
 637                    user.CastReceiverId = config.CastReceiverId;
 638                }
 639
 640                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 641                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 642                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 643                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 644
 645                dbContext.Update(user);
 646                _users[user.Id] = user;
 647                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 648            }
 649        }
 650
 651        /// <inheritdoc/>
 652        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 653        {
 654            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 655            await using (dbContext.ConfigureAwait(false))
 656            {
 657                var user = dbContext.Users
 658                               .Include(u => u.Permissions)
 659                               .Include(u => u.Preferences)
 660                               .Include(u => u.AccessSchedules)
 661                               .Include(u => u.ProfileImage)
 662                               .FirstOrDefault(u => u.Id.Equals(userId))
 663                           ?? throw new ArgumentException("No user exists with given Id!");
 664
 665                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server a
 666                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 667                {
 668                    -1 => null,
 669                    0 => 3,
 670                    _ => policy.LoginAttemptsBeforeLockout
 671                };
 672
 673                user.MaxParentalRatingScore = policy.MaxParentalRating;
 674                user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 675                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 676                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 677                user.AuthenticationProviderId = policy.AuthenticationProviderId;
 678                user.PasswordResetProviderId = policy.PasswordResetProviderId;
 679                user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 680                user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 681                user.MaxActiveSessions = policy.MaxActiveSessions;
 682                user.SyncPlayAccess = policy.SyncPlayAccess;
 683                user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 684                user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 685                user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 686                user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 687                user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 688                user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 689                user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 690                user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 691                user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding)
 692                user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding)
 693                user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 694                user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 695                user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 696                user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 697                user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 698                user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 699                user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 700                user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUser
 701                user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 702                user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 703                user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 704                user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 705                user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
 706                user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 707
 708                user.AccessSchedules.Clear();
 709                foreach (var policyAccessSchedule in policy.AccessSchedules)
 710                {
 711                    user.AccessSchedules.Add(policyAccessSchedule);
 712                }
 713
 714                // TODO: fix this at some point
 715                user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem
 716                user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 717                user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 718                user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 719                user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 720                user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 721                user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFold
 722
 723                dbContext.Update(user);
 724                _users[user.Id] = user;
 725                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 726            }
 727        }
 728
 729        /// <inheritdoc/>
 730        public async Task ClearProfileImageAsync(User user)
 731        {
 732            if (user.ProfileImage is null)
 733            {
 734                return;
 735            }
 736
 737            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 738            await using (dbContext.ConfigureAwait(false))
 739            {
 740                dbContext.Remove(user.ProfileImage);
 741                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 742            }
 743
 744            user.ProfileImage = null;
 745            _users[user.Id] = user;
 746        }
 747
 748        internal static void ThrowIfInvalidUsername(string name)
 749        {
 15750            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
 751            {
 7752                return;
 753            }
 754
 8755            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 756        }
 757
 758        private IAuthenticationProvider GetAuthenticationProvider(User user)
 759        {
 41760            return GetAuthenticationProviders(user)[0];
 761        }
 762
 763        private IPasswordResetProvider GetPasswordResetProvider(User user)
 764        {
 0765            return GetPasswordResetProviders(user)[0];
 766        }
 767
 768        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 769        {
 56770            var authenticationProviderId = user?.AuthenticationProviderId;
 771
 56772            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 773
 56774            if (!string.IsNullOrEmpty(authenticationProviderId))
 775            {
 56776                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 777            }
 778
 56779            if (providers.Count == 0)
 780            {
 781                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0782                _logger.LogWarning(
 0783                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0784                    user?.Username,
 0785                    user?.AuthenticationProviderId);
 0786                providers = new List<IAuthenticationProvider>
 0787                {
 0788                    _invalidAuthProvider
 0789                };
 790            }
 791
 56792            return providers;
 793        }
 794
 795        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 796        {
 0797            var passwordResetProviderId = user.PasswordResetProviderId;
 0798            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 799
 0800            if (!string.IsNullOrEmpty(passwordResetProviderId))
 801            {
 0802                providers = providers.Where(i =>
 0803                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0804                    .ToArray();
 805            }
 806
 0807            if (providers.Length == 0)
 808            {
 0809                providers = new IPasswordResetProvider[]
 0810                {
 0811                    _defaultPasswordResetProvider
 0812                };
 813            }
 814
 0815            return providers;
 816        }
 817
 818        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 819                string username,
 820                string password,
 821                User? user)
 822        {
 823            bool success = false;
 824            IAuthenticationProvider? authenticationProvider = null;
 825
 826            foreach (var provider in GetAuthenticationProviders(user))
 827            {
 828                var providerAuthResult =
 829                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 830                var updatedUsername = providerAuthResult.Username;
 831                success = providerAuthResult.Success;
 832
 833                if (success)
 834                {
 835                    authenticationProvider = provider;
 836                    username = updatedUsername;
 837                    break;
 838                }
 839            }
 840
 841            return (authenticationProvider, username, success);
 842        }
 843
 844        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 845            IAuthenticationProvider provider,
 846            string username,
 847            string password,
 848            User? resolvedUser)
 849        {
 850            try
 851            {
 852                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 853                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 854                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 855
 856                if (authenticationResult.Username != username)
 857                {
 858                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 859                    username = authenticationResult.Username;
 860                }
 861
 862                return (username, true);
 863            }
 864            catch (AuthenticationException ex)
 865            {
 866                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 867
 868                return (username, false);
 869            }
 870        }
 871
 872        private async Task IncrementInvalidLoginAttemptCount(User user)
 873        {
 874            user.InvalidLoginAttemptCount++;
 875            int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 876            if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 877            {
 878                user.SetPermission(PermissionKind.IsDisabled, true);
 879                await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 880                _logger.LogWarning(
 881                    "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 882                    user.Username,
 883                    user.InvalidLoginAttemptCount);
 884            }
 885
 886            await UpdateUserAsync(user).ConfigureAwait(false);
 887        }
 888
 889        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 890        {
 891            dbContext.Users.Attach(user);
 892            dbContext.Entry(user).State = EntityState.Modified;
 893            _users[user.Id] = user;
 894            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 895        }
 896    }
 897}