< 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: 119
Uncovered lines: 46
Coverable lines: 165
Total lines: 884
Line coverage: 72.1%
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%2.06275%
GetUserByName(...)50%2.15266.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%28.671042.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.Entities;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Data.Events.Users;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common;
 16using MediaBrowser.Common.Extensions;
 17using MediaBrowser.Common.Net;
 18using MediaBrowser.Controller.Authentication;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Drawing;
 21using MediaBrowser.Controller.Events;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.Net;
 24using MediaBrowser.Model.Configuration;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.Users;
 27using Microsoft.EntityFrameworkCore;
 28using Microsoft.Extensions.Logging;
 29
 30namespace Jellyfin.Server.Implementations.Users
 31{
 32    /// <summary>
 33    /// Manages the creation and retrieval of <see cref="User"/> instances.
 34    /// </summary>
 35    public partial class UserManager : IUserManager
 36    {
 37        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 38        private readonly IEventManager _eventManager;
 39        private readonly INetworkManager _networkManager;
 40        private readonly IApplicationHost _appHost;
 41        private readonly IImageProcessor _imageProcessor;
 42        private readonly ILogger<UserManager> _logger;
 43        private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
 44        private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
 45        private readonly InvalidAuthProvider _invalidAuthProvider;
 46        private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
 47        private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
 48        private readonly IServerConfigurationManager _serverConfigurationManager;
 49
 50        private readonly IDictionary<Guid, User> _users;
 51
 52        /// <summary>
 53        /// Initializes a new instance of the <see cref="UserManager"/> class.
 54        /// </summary>
 55        /// <param name="dbProvider">The database provider.</param>
 56        /// <param name="eventManager">The event manager.</param>
 57        /// <param name="networkManager">The network manager.</param>
 58        /// <param name="appHost">The application host.</param>
 59        /// <param name="imageProcessor">The image processor.</param>
 60        /// <param name="logger">The logger.</param>
 61        /// <param name="serverConfigurationManager">The system config manager.</param>
 62        /// <param name="passwordResetProviders">The password reset providers.</param>
 63        /// <param name="authenticationProviders">The authentication providers.</param>
 64        public UserManager(
 65            IDbContextFactory<JellyfinDbContext> dbProvider,
 66            IEventManager eventManager,
 67            INetworkManager networkManager,
 68            IApplicationHost appHost,
 69            IImageProcessor imageProcessor,
 70            ILogger<UserManager> logger,
 71            IServerConfigurationManager serverConfigurationManager,
 72            IEnumerable<IPasswordResetProvider> passwordResetProviders,
 73            IEnumerable<IAuthenticationProvider> authenticationProviders)
 74        {
 2275            _dbProvider = dbProvider;
 2276            _eventManager = eventManager;
 2277            _networkManager = networkManager;
 2278            _appHost = appHost;
 2279            _imageProcessor = imageProcessor;
 2280            _logger = logger;
 2281            _serverConfigurationManager = serverConfigurationManager;
 82
 2283            _passwordResetProviders = passwordResetProviders.ToList();
 2284            _authenticationProviders = authenticationProviders.ToList();
 85
 2286            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
 2287            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
 2288            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 89
 2290            _users = new ConcurrentDictionary<Guid, User>();
 2291            using var dbContext = _dbProvider.CreateDbContext();
 4492            foreach (var user in dbContext.Users
 2293                .AsSplitQuery()
 2294                .Include(user => user.Permissions)
 2295                .Include(user => user.Preferences)
 2296                .Include(user => user.AccessSchedules)
 2297                .Include(user => user.ProfileImage)
 2298                .AsEnumerable())
 99            {
 0100                _users.Add(user.Id, user);
 101            }
 22102        }
 103
 104        /// <inheritdoc/>
 105        public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
 106
 107        /// <inheritdoc/>
 63108        public IEnumerable<User> Users => _users.Values;
 109
 110        /// <inheritdoc/>
 0111        public IEnumerable<Guid> UsersIds => _users.Keys;
 112
 113        // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
 114        // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
 115        // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes
 116        [GeneratedRegex(@"^[\w\ \-'._@]+$")]
 117        private static partial Regex ValidUsernameRegex();
 118
 119        /// <inheritdoc/>
 120        public User? GetUserById(Guid id)
 121        {
 284122            if (id.IsEmpty())
 123            {
 0124                throw new ArgumentException("Guid can't be empty", nameof(id));
 125            }
 126
 284127            _users.TryGetValue(id, out var user);
 284128            return user;
 129        }
 130
 131        /// <inheritdoc/>
 132        public User? GetUserByName(string name)
 133        {
 16134            if (string.IsNullOrWhiteSpace(name))
 135            {
 0136                throw new ArgumentException("Invalid username", nameof(name));
 137            }
 138
 16139            return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)
 140        }
 141
 142        /// <inheritdoc/>
 143        public async Task RenameUser(User user, string newName)
 144        {
 145            ArgumentNullException.ThrowIfNull(user);
 146
 147            ThrowIfInvalidUsername(newName);
 148
 149            if (user.Username.Equals(newName, StringComparison.Ordinal))
 150            {
 151                throw new ArgumentException("The new and old names must be different.");
 152            }
 153
 154            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 155            await using (dbContext.ConfigureAwait(false))
 156            {
 157                if (await dbContext.Users
 158                        .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
 159                        .ConfigureAwait(false))
 160                {
 161                    throw new ArgumentException(string.Format(
 162                        CultureInfo.InvariantCulture,
 163                        "A user with the name '{0}' already exists.",
 164                        newName));
 165                }
 166
 167                user.Username = newName;
 168                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 169            }
 170
 171            var eventArgs = new UserUpdatedEventArgs(user);
 172            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 173            OnUserUpdated?.Invoke(this, eventArgs);
 174        }
 175
 176        /// <inheritdoc/>
 177        public async Task UpdateUserAsync(User user)
 178        {
 179            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 180            await using (dbContext.ConfigureAwait(false))
 181            {
 182                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
 183            }
 184        }
 185
 186        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
 187        {
 188            // TODO: Remove after user item data is migrated.
 189            var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
 190                ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
 191                : 0;
 192
 193            var user = new User(
 194                name,
 195                _defaultAuthenticationProvider.GetType().FullName!,
 196                _defaultPasswordResetProvider.GetType().FullName!)
 197            {
 198                InternalId = max + 1
 199            };
 200
 201            user.AddDefaultPermissions();
 202            user.AddDefaultPreferences();
 203
 204            _users.Add(user.Id, user);
 205
 206            return user;
 207        }
 208
 209        /// <inheritdoc/>
 210        public async Task<User> CreateUserAsync(string name)
 211        {
 212            ThrowIfInvalidUsername(name);
 213
 214            if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
 215            {
 216                throw new ArgumentException(string.Format(
 217                    CultureInfo.InvariantCulture,
 218                    "A user with the name '{0}' already exists.",
 219                    name));
 220            }
 221
 222            User newUser;
 223            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 224            await using (dbContext.ConfigureAwait(false))
 225            {
 226                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 227
 228                dbContext.Users.Add(newUser);
 229                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 230            }
 231
 232            await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 233
 234            return newUser;
 235        }
 236
 237        /// <inheritdoc/>
 238        public async Task DeleteUserAsync(Guid userId)
 239        {
 240            if (!_users.TryGetValue(userId, out var user))
 241            {
 242                throw new ResourceNotFoundException(nameof(userId));
 243            }
 244
 245            if (_users.Count == 1)
 246            {
 247                throw new InvalidOperationException(string.Format(
 248                    CultureInfo.InvariantCulture,
 249                    "The user '{0}' cannot be deleted because there must be at least one user in the system.",
 250                    user.Username));
 251            }
 252
 253            if (user.HasPermission(PermissionKind.IsAdministrator)
 254                && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
 255            {
 256                throw new ArgumentException(
 257                    string.Format(
 258                        CultureInfo.InvariantCulture,
 259                        "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
 260                        user.Username),
 261                    nameof(userId));
 262            }
 263
 264            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 265            await using (dbContext.ConfigureAwait(false))
 266            {
 267                dbContext.Users.Remove(user);
 268                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 269            }
 270
 271            _users.Remove(userId);
 272
 273            await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
 274        }
 275
 276        /// <inheritdoc/>
 277        public Task ResetPassword(User user)
 278        {
 0279            return ChangePassword(user, string.Empty);
 280        }
 281
 282        /// <inheritdoc/>
 283        public async Task ChangePassword(User user, string newPassword)
 284        {
 285            ArgumentNullException.ThrowIfNull(user);
 286            if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
 287            {
 288                throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
 289            }
 290
 291            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
 292            await UpdateUserAsync(user).ConfigureAwait(false);
 293
 294            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
 295        }
 296
 297        /// <inheritdoc/>
 298        public UserDto GetUserDto(User user, string? remoteEndPoint = null)
 299        {
 43300            var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
 43301            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 43302            return new UserDto
 43303            {
 43304                Name = user.Username,
 43305                Id = user.Id,
 43306                ServerId = _appHost.SystemId,
 43307                HasPassword = hasPassword,
 43308                HasConfiguredPassword = hasPassword,
 43309                EnableAutoLogin = user.EnableAutoLogin,
 43310                LastLoginDate = user.LastLoginDate,
 43311                LastActivityDate = user.LastActivityDate,
 43312                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 43313                Configuration = new UserConfiguration
 43314                {
 43315                    SubtitleMode = user.SubtitleMode,
 43316                    HidePlayedInLatest = user.HidePlayedInLatest,
 43317                    EnableLocalPassword = user.EnableLocalPassword,
 43318                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 43319                    DisplayCollectionsView = user.DisplayCollectionsView,
 43320                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 43321                    AudioLanguagePreference = user.AudioLanguagePreference,
 43322                    RememberAudioSelections = user.RememberAudioSelections,
 43323                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 43324                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 43325                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 43326                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 43327                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 43328                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 43329                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 43330                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 43331                        ? castReceiverApplications.FirstOrDefault()?.Id
 43332                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 43333                          ?? castReceiverApplications.FirstOrDefault()?.Id
 43334                },
 43335                Policy = new UserPolicy
 43336                {
 43337                    MaxParentalRating = user.MaxParentalAgeRating,
 43338                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 43339                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 43340                    AuthenticationProviderId = user.AuthenticationProviderId,
 43341                    PasswordResetProviderId = user.PasswordResetProviderId,
 43342                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 43343                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 43344                    MaxActiveSessions = user.MaxActiveSessions,
 43345                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 43346                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 43347                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 43348                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 43349                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 43350                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 43351                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 43352                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 43353                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 43354                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 43355                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 43356                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 43357                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 43358                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 43359                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 43360                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 43361                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 43362                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 43363                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 43364                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 43365                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 43366                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 43367                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 43368                    AccessSchedules = user.AccessSchedules.ToArray(),
 43369                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 43370                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 43371                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 43372                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 43373                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 43374                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 43375                    SyncPlayAccess = user.SyncPlayAccess,
 43376                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 43377                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 43378                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 43379                }
 43380            };
 381        }
 382
 383        /// <inheritdoc/>
 384        public async Task<User?> AuthenticateUser(
 385            string username,
 386            string password,
 387            string remoteEndPoint,
 388            bool isUserSession)
 389        {
 390            if (string.IsNullOrWhiteSpace(username))
 391            {
 392                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndP
 393                throw new ArgumentNullException(nameof(username));
 394            }
 395
 396            var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)
 397            var authResult = await AuthenticateLocalUser(username, password, user)
 398                .ConfigureAwait(false);
 399            var authenticationProvider = authResult.AuthenticationProvider;
 400            var success = authResult.Success;
 401
 402            if (user is null)
 403            {
 404                string updatedUsername = authResult.Username;
 405
 406                if (success
 407                    && authenticationProvider is not null
 408                    && authenticationProvider is not DefaultAuthenticationProvider)
 409                {
 410                    // Trust the username returned by the authentication provider
 411                    username = updatedUsername;
 412
 413                    // Search the database for the user again
 414                    // the authentication provider might have created it
 415                    user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreC
 416
 417                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
 418                    {
 419                        await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
 420                    }
 421                }
 422            }
 423
 424            if (success && user is not null && authenticationProvider is not null)
 425            {
 426                var providerId = authenticationProvider.GetType().FullName;
 427
 428                if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison
 429                {
 430                    user.AuthenticationProviderId = providerId;
 431                    await UpdateUserAsync(user).ConfigureAwait(false);
 432                }
 433            }
 434
 435            if (user is null)
 436            {
 437                _logger.LogInformation(
 438                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 439                    username,
 440                    remoteEndPoint);
 441                throw new AuthenticationException("Invalid username or password entered.");
 442            }
 443
 444            if (user.HasPermission(PermissionKind.IsDisabled))
 445            {
 446                _logger.LogInformation(
 447                    "Authentication request for {UserName} has been denied because this account is currently disabled (I
 448                    username,
 449                    remoteEndPoint);
 450                throw new SecurityException(
 451                    $"The {user.Username} account is currently disabled. Please consult with your administrator.");
 452            }
 453
 454            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 455                !_networkManager.IsInLocalNetwork(remoteEndPoint))
 456            {
 457                _logger.LogInformation(
 458                    "Authentication request for {UserName} forbidden: remote access disabled and user not in local netwo
 459                    username,
 460                    remoteEndPoint);
 461                throw new SecurityException("Forbidden.");
 462            }
 463
 464            if (!user.IsParentalScheduleAllowed())
 465            {
 466                _logger.LogInformation(
 467                    "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {I
 468                    username,
 469                    remoteEndPoint);
 470                throw new SecurityException("User is not allowed access at this time.");
 471            }
 472
 473            // Update LastActivityDate and LastLoginDate, then save
 474            if (success)
 475            {
 476                if (isUserSession)
 477                {
 478                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
 479                }
 480
 481                user.InvalidLoginAttemptCount = 0;
 482                await UpdateUserAsync(user).ConfigureAwait(false);
 483                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 484            }
 485            else
 486            {
 487                await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
 488                _logger.LogInformation(
 489                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 490                    user.Username,
 491                    remoteEndPoint);
 492            }
 493
 494            return success ? user : null;
 495        }
 496
 497        /// <inheritdoc/>
 498        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 499        {
 500            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 501
 502            if (user is not null && isInNetwork)
 503            {
 504                var passwordResetProvider = GetPasswordResetProvider(user);
 505                var result = await passwordResetProvider
 506                    .StartForgotPasswordProcess(user, isInNetwork)
 507                    .ConfigureAwait(false);
 508
 509                await UpdateUserAsync(user).ConfigureAwait(false);
 510                return result;
 511            }
 512
 513            return new ForgotPasswordResult
 514            {
 515                Action = ForgotPasswordAction.InNetworkRequired,
 516                PinFile = string.Empty
 517            };
 518        }
 519
 520        /// <inheritdoc/>
 521        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 522        {
 523            foreach (var provider in _passwordResetProviders)
 524            {
 525                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 526
 527                if (result.Success)
 528                {
 529                    return result;
 530                }
 531            }
 532
 533            return new PinRedeemResult();
 534        }
 535
 536        /// <inheritdoc />
 537        public async Task InitializeAsync()
 538        {
 539            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 540            if (_users.Any())
 541            {
 542                return;
 543            }
 544
 545            var defaultName = Environment.UserName;
 546            if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 547            {
 548                defaultName = "MyJellyfinUser";
 549            }
 550
 551            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 552
 553            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 554            await using (dbContext.ConfigureAwait(false))
 555            {
 556                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 557                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 558                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 559                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 560
 561                dbContext.Users.Add(newUser);
 562                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 563            }
 564        }
 565
 566        /// <inheritdoc/>
 567        public NameIdPair[] GetAuthenticationProviders()
 568        {
 0569            return _authenticationProviders
 0570                .Where(provider => provider.IsEnabled)
 0571                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0572                .ThenBy(i => i.Name)
 0573                .Select(i => new NameIdPair
 0574                {
 0575                    Name = i.Name,
 0576                    Id = i.GetType().FullName
 0577                })
 0578                .ToArray();
 579        }
 580
 581        /// <inheritdoc/>
 582        public NameIdPair[] GetPasswordResetProviders()
 583        {
 0584            return _passwordResetProviders
 0585                .Where(provider => provider.IsEnabled)
 0586                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0587                .ThenBy(i => i.Name)
 0588                .Select(i => new NameIdPair
 0589                {
 0590                    Name = i.Name,
 0591                    Id = i.GetType().FullName
 0592                })
 0593                .ToArray();
 594        }
 595
 596        /// <inheritdoc/>
 597        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 598        {
 599            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 600            await using (dbContext.ConfigureAwait(false))
 601            {
 602                var user = dbContext.Users
 603                               .Include(u => u.Permissions)
 604                               .Include(u => u.Preferences)
 605                               .Include(u => u.AccessSchedules)
 606                               .Include(u => u.ProfileImage)
 607                               .FirstOrDefault(u => u.Id.Equals(userId))
 608                           ?? throw new ArgumentException("No user exists with given Id!");
 609
 610                user.SubtitleMode = config.SubtitleMode;
 611                user.HidePlayedInLatest = config.HidePlayedInLatest;
 612                user.EnableLocalPassword = config.EnableLocalPassword;
 613                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 614                user.DisplayCollectionsView = config.DisplayCollectionsView;
 615                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 616                user.AudioLanguagePreference = config.AudioLanguagePreference;
 617                user.RememberAudioSelections = config.RememberAudioSelections;
 618                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 619                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 620                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 621
 622                // Only set cast receiver id if it is passed in and it exists in the server config.
 623                if (!string.IsNullOrEmpty(config.CastReceiverId)
 624                    && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, c
 625                {
 626                    user.CastReceiverId = config.CastReceiverId;
 627                }
 628
 629                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 630                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 631                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 632                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 633
 634                dbContext.Update(user);
 635                _users[user.Id] = user;
 636                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 637            }
 638        }
 639
 640        /// <inheritdoc/>
 641        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 642        {
 643            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 644            await using (dbContext.ConfigureAwait(false))
 645            {
 646                var user = dbContext.Users
 647                               .Include(u => u.Permissions)
 648                               .Include(u => u.Preferences)
 649                               .Include(u => u.AccessSchedules)
 650                               .Include(u => u.ProfileImage)
 651                               .FirstOrDefault(u => u.Id.Equals(userId))
 652                           ?? throw new ArgumentException("No user exists with given Id!");
 653
 654                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server a
 655                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 656                {
 657                    -1 => null,
 658                    0 => 3,
 659                    _ => policy.LoginAttemptsBeforeLockout
 660                };
 661
 662                user.MaxParentalAgeRating = policy.MaxParentalRating;
 663                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 664                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 665                user.AuthenticationProviderId = policy.AuthenticationProviderId;
 666                user.PasswordResetProviderId = policy.PasswordResetProviderId;
 667                user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 668                user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 669                user.MaxActiveSessions = policy.MaxActiveSessions;
 670                user.SyncPlayAccess = policy.SyncPlayAccess;
 671                user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 672                user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 673                user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 674                user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 675                user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 676                user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 677                user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 678                user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 679                user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding)
 680                user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding)
 681                user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 682                user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 683                user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 684                user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 685                user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 686                user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 687                user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 688                user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUser
 689                user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 690                user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 691                user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 692                user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 693                user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
 694                user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 695
 696                user.AccessSchedules.Clear();
 697                foreach (var policyAccessSchedule in policy.AccessSchedules)
 698                {
 699                    user.AccessSchedules.Add(policyAccessSchedule);
 700                }
 701
 702                // TODO: fix this at some point
 703                user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem
 704                user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 705                user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 706                user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 707                user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 708                user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 709                user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFold
 710
 711                dbContext.Update(user);
 712                _users[user.Id] = user;
 713                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 714            }
 715        }
 716
 717        /// <inheritdoc/>
 718        public async Task ClearProfileImageAsync(User user)
 719        {
 720            if (user.ProfileImage is null)
 721            {
 722                return;
 723            }
 724
 725            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 726            await using (dbContext.ConfigureAwait(false))
 727            {
 728                dbContext.Remove(user.ProfileImage);
 729                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 730            }
 731
 732            user.ProfileImage = null;
 733            _users[user.Id] = user;
 734        }
 735
 736        internal static void ThrowIfInvalidUsername(string name)
 737        {
 8738            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
 739            {
 4740                return;
 741            }
 742
 4743            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 744        }
 745
 746        private IAuthenticationProvider GetAuthenticationProvider(User user)
 747        {
 46748            return GetAuthenticationProviders(user)[0];
 749        }
 750
 751        private IPasswordResetProvider GetPasswordResetProvider(User user)
 752        {
 0753            return GetPasswordResetProviders(user)[0];
 754        }
 755
 756        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 757        {
 62758            var authenticationProviderId = user?.AuthenticationProviderId;
 759
 62760            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 761
 62762            if (!string.IsNullOrEmpty(authenticationProviderId))
 763            {
 62764                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 765            }
 766
 62767            if (providers.Count == 0)
 768            {
 769                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0770                _logger.LogWarning(
 0771                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0772                    user?.Username,
 0773                    user?.AuthenticationProviderId);
 0774                providers = new List<IAuthenticationProvider>
 0775                {
 0776                    _invalidAuthProvider
 0777                };
 778            }
 779
 62780            return providers;
 781        }
 782
 783        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 784        {
 0785            var passwordResetProviderId = user.PasswordResetProviderId;
 0786            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 787
 0788            if (!string.IsNullOrEmpty(passwordResetProviderId))
 789            {
 0790                providers = providers.Where(i =>
 0791                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0792                    .ToArray();
 793            }
 794
 0795            if (providers.Length == 0)
 796            {
 0797                providers = new IPasswordResetProvider[]
 0798                {
 0799                    _defaultPasswordResetProvider
 0800                };
 801            }
 802
 0803            return providers;
 804        }
 805
 806        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 807                string username,
 808                string password,
 809                User? user)
 810        {
 811            bool success = false;
 812            IAuthenticationProvider? authenticationProvider = null;
 813
 814            foreach (var provider in GetAuthenticationProviders(user))
 815            {
 816                var providerAuthResult =
 817                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 818                var updatedUsername = providerAuthResult.Username;
 819                success = providerAuthResult.Success;
 820
 821                if (success)
 822                {
 823                    authenticationProvider = provider;
 824                    username = updatedUsername;
 825                    break;
 826                }
 827            }
 828
 829            return (authenticationProvider, username, success);
 830        }
 831
 832        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 833            IAuthenticationProvider provider,
 834            string username,
 835            string password,
 836            User? resolvedUser)
 837        {
 838            try
 839            {
 840                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 841                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 842                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 843
 844                if (authenticationResult.Username != username)
 845                {
 846                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 847                    username = authenticationResult.Username;
 848                }
 849
 850                return (username, true);
 851            }
 852            catch (AuthenticationException ex)
 853            {
 854                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 855
 856                return (username, false);
 857            }
 858        }
 859
 860        private async Task IncrementInvalidLoginAttemptCount(User user)
 861        {
 862            user.InvalidLoginAttemptCount++;
 863            int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 864            if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 865            {
 866                user.SetPermission(PermissionKind.IsDisabled, true);
 867                await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 868                _logger.LogWarning(
 869                    "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 870                    user.Username,
 871                    user.InvalidLoginAttemptCount);
 872            }
 873
 874            await UpdateUserAsync(user).ConfigureAwait(false);
 875        }
 876
 877        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 878        {
 879            dbContext.Users.Update(user);
 880            _users[user.Id] = user;
 881            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 882        }
 883    }
 884}