< 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
70%
Covered lines: 117
Uncovered lines: 48
Coverable lines: 165
Total lines: 894
Line coverage: 70.9%
Branch coverage
42%
Covered branches: 17
Total branches: 40
Branch coverage: 42.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 9/14/2025 - 12:09:49 AM Line coverage: 72.2% (120/166) Branch coverage: 44.7% (17/38) Total lines: 8959/21/2025 - 12:11:25 AM Line coverage: 72.2% (120/166) Branch coverage: 44.7% (17/38) Total lines: 89711/4/2025 - 12:11:59 AM Line coverage: 71.4% (120/168) Branch coverage: 42.5% (17/40) Total lines: 89711/28/2025 - 12:11:11 AM Line coverage: 70.9% (117/165) Branch coverage: 42.5% (17/40) Total lines: 894 9/14/2025 - 12:09:49 AM Line coverage: 72.2% (120/166) Branch coverage: 44.7% (17/38) Total lines: 8959/21/2025 - 12:11:25 AM Line coverage: 72.2% (120/166) Branch coverage: 44.7% (17/38) Total lines: 89711/4/2025 - 12:11:59 AM Line coverage: 71.4% (120/168) Branch coverage: 42.5% (17/40) Total lines: 89711/28/2025 - 12:11:11 AM Line coverage: 70.9% (117/165) Branch coverage: 42.5% (17/40) Total lines: 894

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(...)0%620%
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/>
 37111        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        {
 243125            if (id.IsEmpty())
 126            {
 0127                throw new ArgumentException("Guid can't be empty", nameof(id));
 128            }
 129
 243130            _users.TryGetValue(id, out var user);
 243131            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        {
 34309            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
 34310            return new UserDto
 34311            {
 34312                Name = user.Username,
 34313                Id = user.Id,
 34314                ServerId = _appHost.SystemId,
 34315                EnableAutoLogin = user.EnableAutoLogin,
 34316                LastLoginDate = user.LastLoginDate,
 34317                LastActivityDate = user.LastActivityDate,
 34318                PrimaryImageTag = user.ProfileImage is not null ? _imageProcessor.GetImageCacheTag(user) : null,
 34319                Configuration = new UserConfiguration
 34320                {
 34321                    SubtitleMode = user.SubtitleMode,
 34322                    HidePlayedInLatest = user.HidePlayedInLatest,
 34323                    EnableLocalPassword = user.EnableLocalPassword,
 34324                    PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
 34325                    DisplayCollectionsView = user.DisplayCollectionsView,
 34326                    DisplayMissingEpisodes = user.DisplayMissingEpisodes,
 34327                    AudioLanguagePreference = user.AudioLanguagePreference,
 34328                    RememberAudioSelections = user.RememberAudioSelections,
 34329                    EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
 34330                    RememberSubtitleSelections = user.RememberSubtitleSelections,
 34331                    SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
 34332                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
 34333                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
 34334                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
 34335                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
 34336                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
 34337                        ? castReceiverApplications.FirstOrDefault()?.Id
 34338                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringCo
 34339                          ?? castReceiverApplications.FirstOrDefault()?.Id
 34340                },
 34341                Policy = new UserPolicy
 34342                {
 34343                    MaxParentalRating = user.MaxParentalRatingScore,
 34344                    MaxParentalSubRating = user.MaxParentalRatingSubScore,
 34345                    EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
 34346                    RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
 34347                    AuthenticationProviderId = user.AuthenticationProviderId,
 34348                    PasswordResetProviderId = user.PasswordResetProviderId,
 34349                    InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
 34350                    LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
 34351                    MaxActiveSessions = user.MaxActiveSessions,
 34352                    IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
 34353                    IsHidden = user.HasPermission(PermissionKind.IsHidden),
 34354                    IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
 34355                    EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
 34356                    EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
 34357                    EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
 34358                    EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
 34359                    EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
 34360                    EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
 34361                    EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
 34362                    EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
 34363                    EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
 34364                    EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
 34365                    EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
 34366                    EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
 34367                    EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
 34368                    EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
 34369                    EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)
 34370                    EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
 34371                    ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
 34372                    EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
 34373                    EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
 34374                    EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
 34375                    AccessSchedules = user.AccessSchedules.ToArray(),
 34376                    BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
 34377                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
 34378                    EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
 34379                    EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
 34380                    EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
 34381                    EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolder
 34382                    SyncPlayAccess = user.SyncPlayAccess,
 34383                    BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
 34384                    BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
 34385                    BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
 34386                }
 34387            };
 388        }
 389
 390        /// <inheritdoc/>
 391        public async Task<User?> AuthenticateUser(
 392            string username,
 393            string password,
 394            string remoteEndPoint,
 395            bool isUserSession)
 396        {
 397            if (string.IsNullOrWhiteSpace(username))
 398            {
 399                _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndP
 400                throw new ArgumentNullException(nameof(username));
 401            }
 402
 403            var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)
 404            var authResult = await AuthenticateLocalUser(username, password, user)
 405                .ConfigureAwait(false);
 406            var authenticationProvider = authResult.AuthenticationProvider;
 407            var success = authResult.Success;
 408
 409            if (user is null)
 410            {
 411                string updatedUsername = authResult.Username;
 412
 413                if (success
 414                    && authenticationProvider is not null
 415                    && authenticationProvider is not DefaultAuthenticationProvider)
 416                {
 417                    // Trust the username returned by the authentication provider
 418                    username = updatedUsername;
 419
 420                    // Search the database for the user again
 421                    // the authentication provider might have created it
 422                    user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreC
 423
 424                    if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
 425                    {
 426                        await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
 427                    }
 428                }
 429            }
 430
 431            if (success && user is not null && authenticationProvider is not null)
 432            {
 433                var providerId = authenticationProvider.GetType().FullName;
 434
 435                if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison
 436                {
 437                    user.AuthenticationProviderId = providerId;
 438                    await UpdateUserAsync(user).ConfigureAwait(false);
 439                }
 440            }
 441
 442            if (user is null)
 443            {
 444                _logger.LogInformation(
 445                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 446                    username,
 447                    remoteEndPoint);
 448                throw new AuthenticationException("Invalid username or password entered.");
 449            }
 450
 451            if (user.HasPermission(PermissionKind.IsDisabled))
 452            {
 453                _logger.LogInformation(
 454                    "Authentication request for {UserName} has been denied because this account is currently disabled (I
 455                    username,
 456                    remoteEndPoint);
 457                throw new SecurityException(
 458                    $"The {user.Username} account is currently disabled. Please consult with your administrator.");
 459            }
 460
 461            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
 462                !_networkManager.IsInLocalNetwork(remoteEndPoint))
 463            {
 464                _logger.LogInformation(
 465                    "Authentication request for {UserName} forbidden: remote access disabled and user not in local netwo
 466                    username,
 467                    remoteEndPoint);
 468                throw new SecurityException("Forbidden.");
 469            }
 470
 471            if (!user.IsParentalScheduleAllowed())
 472            {
 473                _logger.LogInformation(
 474                    "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {I
 475                    username,
 476                    remoteEndPoint);
 477                throw new SecurityException("User is not allowed access at this time.");
 478            }
 479
 480            // Update LastActivityDate and LastLoginDate, then save
 481            if (success)
 482            {
 483                if (isUserSession)
 484                {
 485                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
 486                }
 487
 488                user.InvalidLoginAttemptCount = 0;
 489                await UpdateUserAsync(user).ConfigureAwait(false);
 490                _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
 491            }
 492            else
 493            {
 494                await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
 495                _logger.LogInformation(
 496                    "Authentication request for {UserName} has been denied (IP: {IP}).",
 497                    user.Username,
 498                    remoteEndPoint);
 499            }
 500
 501            return success ? user : null;
 502        }
 503
 504        /// <inheritdoc/>
 505        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
 506        {
 507            var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
 508            var passwordResetProvider = GetPasswordResetProvider(user);
 509
 510            var result = await passwordResetProvider
 511                .StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
 512                .ConfigureAwait(false);
 513
 514            if (user is not null && isInNetwork)
 515            {
 516                await UpdateUserAsync(user).ConfigureAwait(false);
 517            }
 518
 519            return result;
 520        }
 521
 522        /// <inheritdoc/>
 523        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
 524        {
 525            foreach (var provider in _passwordResetProviders)
 526            {
 527                var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
 528
 529                if (result.Success)
 530                {
 531                    return result;
 532                }
 533            }
 534
 535            return new PinRedeemResult();
 536        }
 537
 538        /// <inheritdoc />
 539        public async Task InitializeAsync()
 540        {
 541            // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
 542            if (_users.Any())
 543            {
 544                return;
 545            }
 546
 547            var defaultName = Environment.UserName;
 548            if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
 549            {
 550                defaultName = "MyJellyfinUser";
 551            }
 552
 553            _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 554
 555            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 556            await using (dbContext.ConfigureAwait(false))
 557            {
 558                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
 559                newUser.SetPermission(PermissionKind.IsAdministrator, true);
 560                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
 561                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 562
 563                dbContext.Users.Add(newUser);
 564                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 565                _users.Add(newUser.Id, newUser);
 566            }
 567        }
 568
 569        /// <inheritdoc/>
 570        public NameIdPair[] GetAuthenticationProviders()
 571        {
 0572            return _authenticationProviders
 0573                .Where(provider => provider.IsEnabled)
 0574                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
 0575                .ThenBy(i => i.Name)
 0576                .Select(i => new NameIdPair
 0577                {
 0578                    Name = i.Name,
 0579                    Id = i.GetType().FullName
 0580                })
 0581                .ToArray();
 582        }
 583
 584        /// <inheritdoc/>
 585        public NameIdPair[] GetPasswordResetProviders()
 586        {
 0587            return _passwordResetProviders
 0588                .Where(provider => provider.IsEnabled)
 0589                .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
 0590                .ThenBy(i => i.Name)
 0591                .Select(i => new NameIdPair
 0592                {
 0593                    Name = i.Name,
 0594                    Id = i.GetType().FullName
 0595                })
 0596                .ToArray();
 597        }
 598
 599        /// <inheritdoc/>
 600        public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
 601        {
 602            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 603            await using (dbContext.ConfigureAwait(false))
 604            {
 605                var user = dbContext.Users
 606                               .Include(u => u.Permissions)
 607                               .Include(u => u.Preferences)
 608                               .Include(u => u.AccessSchedules)
 609                               .Include(u => u.ProfileImage)
 610                               .FirstOrDefault(u => u.Id.Equals(userId))
 611                           ?? throw new ArgumentException("No user exists with given Id!");
 612
 613                user.SubtitleMode = config.SubtitleMode;
 614                user.HidePlayedInLatest = config.HidePlayedInLatest;
 615                user.EnableLocalPassword = config.EnableLocalPassword;
 616                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
 617                user.DisplayCollectionsView = config.DisplayCollectionsView;
 618                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
 619                user.AudioLanguagePreference = config.AudioLanguagePreference;
 620                user.RememberAudioSelections = config.RememberAudioSelections;
 621                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
 622                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
 623                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 624
 625                // Only set cast receiver id if it is passed in and it exists in the server config.
 626                if (!string.IsNullOrEmpty(config.CastReceiverId)
 627                    && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, c
 628                {
 629                    user.CastReceiverId = config.CastReceiverId;
 630                }
 631
 632                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
 633                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
 634                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
 635                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
 636
 637                dbContext.Update(user);
 638                _users[user.Id] = user;
 639                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 640            }
 641        }
 642
 643        /// <inheritdoc/>
 644        public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
 645        {
 646            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 647            await using (dbContext.ConfigureAwait(false))
 648            {
 649                var user = dbContext.Users
 650                               .Include(u => u.Permissions)
 651                               .Include(u => u.Preferences)
 652                               .Include(u => u.AccessSchedules)
 653                               .Include(u => u.ProfileImage)
 654                               .FirstOrDefault(u => u.Id.Equals(userId))
 655                           ?? throw new ArgumentException("No user exists with given Id!");
 656
 657                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server a
 658                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
 659                {
 660                    -1 => null,
 661                    0 => 3,
 662                    _ => policy.LoginAttemptsBeforeLockout
 663                };
 664
 665                user.MaxParentalRatingScore = policy.MaxParentalRating;
 666                user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
 667                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
 668                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
 669                user.AuthenticationProviderId = policy.AuthenticationProviderId;
 670                user.PasswordResetProviderId = policy.PasswordResetProviderId;
 671                user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
 672                user.LoginAttemptsBeforeLockout = maxLoginAttempts;
 673                user.MaxActiveSessions = policy.MaxActiveSessions;
 674                user.SyncPlayAccess = policy.SyncPlayAccess;
 675                user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
 676                user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
 677                user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
 678                user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
 679                user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
 680                user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
 681                user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
 682                user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
 683                user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding)
 684                user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding)
 685                user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
 686                user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
 687                user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
 688                user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
 689                user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
 690                user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
 691                user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
 692                user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUser
 693                user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
 694                user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
 695                user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
 696                user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
 697                user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
 698                user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
 699
 700                user.AccessSchedules.Clear();
 701                foreach (var policyAccessSchedule in policy.AccessSchedules)
 702                {
 703                    user.AccessSchedules.Add(policyAccessSchedule);
 704                }
 705
 706                // TODO: fix this at some point
 707                user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem
 708                user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
 709                user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
 710                user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
 711                user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
 712                user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
 713                user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFold
 714
 715                dbContext.Update(user);
 716                _users[user.Id] = user;
 717                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 718            }
 719        }
 720
 721        /// <inheritdoc/>
 722        public async Task ClearProfileImageAsync(User user)
 723        {
 724            if (user.ProfileImage is null)
 725            {
 726                return;
 727            }
 728
 729            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 730            await using (dbContext.ConfigureAwait(false))
 731            {
 732                dbContext.Remove(user.ProfileImage);
 733                await dbContext.SaveChangesAsync().ConfigureAwait(false);
 734            }
 735
 736            user.ProfileImage = null;
 737            _users[user.Id] = user;
 738        }
 739
 740        internal static void ThrowIfInvalidUsername(string name)
 741        {
 15742            if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
 743            {
 7744                return;
 745            }
 746
 8747            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (
 748        }
 749
 750        private IAuthenticationProvider GetAuthenticationProvider(User user)
 751        {
 3752            return GetAuthenticationProviders(user)[0];
 753        }
 754
 755        private IPasswordResetProvider GetPasswordResetProvider(User? user)
 756        {
 0757            if (user is null)
 758            {
 0759                return _defaultPasswordResetProvider;
 760            }
 761
 0762            return GetPasswordResetProviders(user)[0];
 763        }
 764
 765        private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
 766        {
 18767            var authenticationProviderId = user?.AuthenticationProviderId;
 768
 18769            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
 770
 18771            if (!string.IsNullOrEmpty(authenticationProviderId))
 772            {
 18773                providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringCom
 774            }
 775
 18776            if (providers.Count == 0)
 777            {
 778                // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
 0779                _logger.LogWarning(
 0780                    "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. 
 0781                    user?.Username,
 0782                    user?.AuthenticationProviderId);
 0783                providers = new List<IAuthenticationProvider>
 0784                {
 0785                    _invalidAuthProvider
 0786                };
 787            }
 788
 18789            return providers;
 790        }
 791
 792        private IPasswordResetProvider[] GetPasswordResetProviders(User user)
 793        {
 0794            var passwordResetProviderId = user.PasswordResetProviderId;
 0795            var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 796
 0797            if (!string.IsNullOrEmpty(passwordResetProviderId))
 798            {
 0799                providers = providers.Where(i =>
 0800                        string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)
 0801                    .ToArray();
 802            }
 803
 0804            if (providers.Length == 0)
 805            {
 0806                providers = new IPasswordResetProvider[]
 0807                {
 0808                    _defaultPasswordResetProvider
 0809                };
 810            }
 811
 0812            return providers;
 813        }
 814
 815        private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> Authenticat
 816                string username,
 817                string password,
 818                User? user)
 819        {
 820            bool success = false;
 821            IAuthenticationProvider? authenticationProvider = null;
 822
 823            foreach (var provider in GetAuthenticationProviders(user))
 824            {
 825                var providerAuthResult =
 826                    await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
 827                var updatedUsername = providerAuthResult.Username;
 828                success = providerAuthResult.Success;
 829
 830                if (success)
 831                {
 832                    authenticationProvider = provider;
 833                    username = updatedUsername;
 834                    break;
 835                }
 836            }
 837
 838            return (authenticationProvider, username, success);
 839        }
 840
 841        private async Task<(string Username, bool Success)> AuthenticateWithProvider(
 842            IAuthenticationProvider provider,
 843            string username,
 844            string password,
 845            User? resolvedUser)
 846        {
 847            try
 848            {
 849                var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
 850                    ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
 851                    : await provider.Authenticate(username, password).ConfigureAwait(false);
 852
 853                if (authenticationResult.Username != username)
 854                {
 855                    _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Usern
 856                    username = authenticationResult.Username;
 857                }
 858
 859                return (username, true);
 860            }
 861            catch (AuthenticationException ex)
 862            {
 863                _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
 864
 865                return (username, false);
 866            }
 867        }
 868
 869        private async Task IncrementInvalidLoginAttemptCount(User user)
 870        {
 871            user.InvalidLoginAttemptCount++;
 872            int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
 873            if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
 874            {
 875                user.SetPermission(PermissionKind.IsDisabled, true);
 876                await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false);
 877                _logger.LogWarning(
 878                    "Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
 879                    user.Username,
 880                    user.InvalidLoginAttemptCount);
 881            }
 882
 883            await UpdateUserAsync(user).ConfigureAwait(false);
 884        }
 885
 886        private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
 887        {
 888            dbContext.Users.Attach(user);
 889            dbContext.Entry(user).State = EntityState.Modified;
 890            _users[user.Id] = user;
 891            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 892        }
 893    }
 894}