2019-11-01 18:38:54 +01:00
#pragma warning disable CS1591
2019-03-17 05:36:45 +01:00
using System ;
2019-06-09 22:08:01 +02:00
using System.Collections.Concurrent ;
2019-03-17 05:36:45 +01:00
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
2019-09-17 18:07:15 +02:00
using MediaBrowser.Common.Cryptography ;
2019-03-17 05:36:45 +01:00
using MediaBrowser.Common.Events ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Controller ;
using MediaBrowser.Controller.Authentication ;
using MediaBrowser.Controller.Devices ;
using MediaBrowser.Controller.Drawing ;
using MediaBrowser.Controller.Dto ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Persistence ;
using MediaBrowser.Controller.Plugins ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Controller.Security ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Model.Configuration ;
2019-11-21 17:32:29 +01:00
using MediaBrowser.Model.Cryptography ;
2019-03-17 05:36:45 +01:00
using MediaBrowser.Model.Dto ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Events ;
using MediaBrowser.Model.IO ;
using MediaBrowser.Model.Serialization ;
using MediaBrowser.Model.Users ;
using Microsoft.Extensions.Logging ;
namespace Emby.Server.Implementations.Library
{
/// <summary>
2019-11-01 18:38:54 +01:00
/// Class UserManager.
2019-03-17 05:36:45 +01:00
/// </summary>
public class UserManager : IUserManager
{
/// <summary>
2019-11-01 18:38:54 +01:00
/// The _logger.
2019-03-17 05:36:45 +01:00
/// </summary>
private readonly ILogger _logger ;
2019-06-09 22:08:01 +02:00
private readonly object _policySyncLock = new object ( ) ;
2019-03-17 05:36:45 +01:00
/// <summary>
2019-11-01 18:38:54 +01:00
/// Gets the active user repository.
2019-03-17 05:36:45 +01:00
/// </summary>
/// <value>The user repository.</value>
2019-06-09 22:08:01 +02:00
private readonly IUserRepository _userRepository ;
2019-03-17 05:36:45 +01:00
private readonly IXmlSerializer _xmlSerializer ;
private readonly IJsonSerializer _jsonSerializer ;
private readonly INetworkManager _networkManager ;
private readonly Func < IImageProcessor > _imageProcessorFactory ;
private readonly Func < IDtoService > _dtoServiceFactory ;
private readonly IServerApplicationHost _appHost ;
private readonly IFileSystem _fileSystem ;
2019-11-21 17:32:29 +01:00
private readonly ICryptoProvider _cryptoProvider ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
private ConcurrentDictionary < Guid , User > _users ;
2019-03-17 05:36:45 +01:00
private IAuthenticationProvider [ ] _authenticationProviders ;
private DefaultAuthenticationProvider _defaultAuthenticationProvider ;
2019-06-09 04:54:31 +02:00
private InvalidAuthProvider _invalidAuthProvider ;
2019-03-22 08:01:23 +01:00
private IPasswordResetProvider [ ] _passwordResetProviders ;
private DefaultPasswordResetProvider _defaultPasswordResetProvider ;
2019-03-17 05:36:45 +01:00
public UserManager (
2019-06-09 22:08:01 +02:00
ILogger < UserManager > logger ,
2019-03-17 05:36:45 +01:00
IUserRepository userRepository ,
IXmlSerializer xmlSerializer ,
INetworkManager networkManager ,
Func < IImageProcessor > imageProcessorFactory ,
Func < IDtoService > dtoServiceFactory ,
IServerApplicationHost appHost ,
IJsonSerializer jsonSerializer ,
2019-11-21 17:32:29 +01:00
IFileSystem fileSystem ,
ICryptoProvider cryptoProvider )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
_logger = logger ;
_userRepository = userRepository ;
2019-03-17 05:36:45 +01:00
_xmlSerializer = xmlSerializer ;
_networkManager = networkManager ;
_imageProcessorFactory = imageProcessorFactory ;
_dtoServiceFactory = dtoServiceFactory ;
_appHost = appHost ;
_jsonSerializer = jsonSerializer ;
_fileSystem = fileSystem ;
2019-11-21 17:32:29 +01:00
_cryptoProvider = cryptoProvider ;
2019-06-09 22:08:01 +02:00
_users = null ;
}
public event EventHandler < GenericEventArgs < User > > UserPasswordChanged ;
/// <summary>
/// Occurs when [user updated].
/// </summary>
public event EventHandler < GenericEventArgs < User > > UserUpdated ;
public event EventHandler < GenericEventArgs < User > > UserPolicyUpdated ;
public event EventHandler < GenericEventArgs < User > > UserConfigurationUpdated ;
public event EventHandler < GenericEventArgs < User > > UserLockedOut ;
public event EventHandler < GenericEventArgs < User > > UserCreated ;
/// <summary>
/// Occurs when [user deleted].
/// </summary>
public event EventHandler < GenericEventArgs < User > > UserDeleted ;
/// <inheritdoc />
public IEnumerable < User > Users = > _users . Values ;
/// <inheritdoc />
public IEnumerable < Guid > UsersIds = > _users . Keys ;
/// <summary>
/// Called when [user updated].
/// </summary>
/// <param name="user">The user.</param>
private void OnUserUpdated ( User user )
{
UserUpdated ? . Invoke ( this , new GenericEventArgs < User > { Argument = user } ) ;
}
/// <summary>
/// Called when [user deleted].
/// </summary>
/// <param name="user">The user.</param>
private void OnUserDeleted ( User user )
{
UserDeleted ? . Invoke ( this , new GenericEventArgs < User > { Argument = user } ) ;
2019-03-17 05:36:45 +01:00
}
public NameIdPair [ ] GetAuthenticationProviders ( )
{
return _authenticationProviders
. Where ( i = > i . IsEnabled )
. OrderBy ( i = > i is DefaultAuthenticationProvider ? 0 : 1 )
. ThenBy ( i = > i . Name )
. Select ( i = > new NameIdPair
{
Name = i . Name ,
Id = GetAuthenticationProviderId ( i )
} )
. ToArray ( ) ;
}
2019-03-22 08:01:23 +01:00
public NameIdPair [ ] GetPasswordResetProviders ( )
{
return _passwordResetProviders
. Where ( i = > i . IsEnabled )
. OrderBy ( i = > i is DefaultPasswordResetProvider ? 0 : 1 )
. ThenBy ( i = > i . Name )
. Select ( i = > new NameIdPair
{
Name = i . Name ,
Id = GetPasswordResetProviderId ( i )
} )
. ToArray ( ) ;
}
2019-06-09 22:08:01 +02:00
public void AddParts ( IEnumerable < IAuthenticationProvider > authenticationProviders , IEnumerable < IPasswordResetProvider > passwordResetProviders )
2019-03-17 05:36:45 +01:00
{
_authenticationProviders = authenticationProviders . ToArray ( ) ;
_defaultAuthenticationProvider = _authenticationProviders . OfType < DefaultAuthenticationProvider > ( ) . First ( ) ;
2019-03-22 08:01:23 +01:00
2019-06-09 04:54:31 +02:00
_invalidAuthProvider = _authenticationProviders . OfType < InvalidAuthProvider > ( ) . First ( ) ;
2019-03-22 08:01:23 +01:00
_passwordResetProviders = passwordResetProviders . ToArray ( ) ;
_defaultPasswordResetProvider = passwordResetProviders . OfType < DefaultPasswordResetProvider > ( ) . First ( ) ;
2019-03-17 05:36:45 +01:00
}
2019-10-20 21:12:03 +02:00
/// <inheritdoc />
2019-03-17 05:36:45 +01:00
public User GetUserById ( Guid id )
{
if ( id = = Guid . Empty )
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( "Guid can't be empty" , nameof ( id ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
_users . TryGetValue ( id , out User user ) ;
return user ;
2019-03-17 05:36:45 +01:00
}
2019-10-20 21:12:03 +02:00
/// <inheritdoc />
2019-03-17 05:36:45 +01:00
public User GetUserById ( string id )
2019-06-09 22:08:01 +02:00
= > GetUserById ( new Guid ( id ) ) ;
2019-03-17 05:36:45 +01:00
public User GetUserByName ( string name )
{
if ( string . IsNullOrWhiteSpace ( name ) )
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( "Invalid username" , nameof ( name ) ) ;
2019-03-17 05:36:45 +01:00
}
return Users . FirstOrDefault ( u = > string . Equals ( u . Name , name , StringComparison . OrdinalIgnoreCase ) ) ;
}
public void Initialize ( )
{
2019-06-09 22:08:01 +02:00
LoadUsers ( ) ;
var users = Users ;
2019-03-17 05:36:45 +01:00
// If there are no local users with admin rights, make them all admins
if ( ! users . Any ( i = > i . Policy . IsAdministrator ) )
{
foreach ( var user in users )
{
user . Policy . IsAdministrator = true ;
UpdateUserPolicy ( user , user . Policy , false ) ;
}
}
}
public static bool IsValidUsername ( string username )
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
2019-06-09 22:08:01 +02:00
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
2019-03-17 05:36:45 +01:00
return Regex . IsMatch ( username , @"^[\w\-'._@]*$" ) ;
}
private static bool IsValidUsernameCharacter ( char i )
2019-06-09 22:08:01 +02:00
= > IsValidUsername ( i . ToString ( CultureInfo . InvariantCulture ) ) ;
2019-03-17 05:36:45 +01:00
public string MakeValidUsername ( string username )
{
if ( IsValidUsername ( username ) )
{
return username ;
}
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
var builder = new StringBuilder ( ) ;
foreach ( var c in username )
{
if ( IsValidUsernameCharacter ( c ) )
{
builder . Append ( c ) ;
}
}
2019-05-21 19:28:34 +02:00
2019-03-17 05:36:45 +01:00
return builder . ToString ( ) ;
}
public async Task < User > AuthenticateUser ( string username , string password , string hashedPassword , string remoteEndPoint , bool isUserSession )
{
if ( string . IsNullOrWhiteSpace ( username ) )
{
throw new ArgumentNullException ( nameof ( username ) ) ;
}
2019-06-09 22:08:01 +02:00
var user = Users . FirstOrDefault ( i = > string . Equals ( username , i . Name , StringComparison . OrdinalIgnoreCase ) ) ;
2019-03-17 05:36:45 +01:00
var success = false ;
IAuthenticationProvider authenticationProvider = null ;
if ( user ! = null )
{
var authResult = await AuthenticateLocalUser ( username , password , hashedPassword , user , remoteEndPoint ) . ConfigureAwait ( false ) ;
2019-05-21 19:28:34 +02:00
authenticationProvider = authResult . authenticationProvider ;
success = authResult . success ;
2019-03-17 05:36:45 +01:00
}
else
{
// user is null
var authResult = await AuthenticateLocalUser ( username , password , hashedPassword , null , remoteEndPoint ) . ConfigureAwait ( false ) ;
2019-05-21 19:28:34 +02:00
authenticationProvider = authResult . authenticationProvider ;
2019-09-20 12:42:08 +02:00
string updatedUsername = authResult . username ;
2019-05-21 19:28:34 +02:00
success = authResult . success ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
if ( success
& & authenticationProvider ! = null
& & ! ( authenticationProvider is DefaultAuthenticationProvider ) )
2019-03-17 05:36:45 +01:00
{
2019-04-08 01:51:45 +02:00
// We should trust the user that the authprovider says, not what was typed
2019-06-09 22:08:01 +02:00
username = updatedUsername ;
2019-04-08 01:51:45 +02:00
// Search the database for the user again; the authprovider might have created it
user = Users
. FirstOrDefault ( i = > string . Equals ( username , i . Name , StringComparison . OrdinalIgnoreCase ) ) ;
2019-03-17 05:36:45 +01:00
2019-06-09 19:57:49 +02:00
if ( authenticationProvider is IHasNewUserPolicy hasNewUserPolicy )
2019-03-17 05:36:45 +01:00
{
2019-06-09 19:57:49 +02:00
var policy = hasNewUserPolicy . GetNewUserPolicy ( ) ;
UpdateUserPolicy ( user , policy , true ) ;
2019-03-17 05:36:45 +01:00
}
}
}
if ( success & & user ! = null & & authenticationProvider ! = null )
{
var providerId = GetAuthenticationProviderId ( authenticationProvider ) ;
if ( ! string . Equals ( providerId , user . Policy . AuthenticationProviderId , StringComparison . OrdinalIgnoreCase ) )
{
user . Policy . AuthenticationProviderId = providerId ;
UpdateUserPolicy ( user , user . Policy , true ) ;
}
}
if ( user = = null )
{
2019-05-21 19:28:34 +02:00
throw new AuthenticationException ( "Invalid username or password entered." ) ;
2019-03-17 05:36:45 +01:00
}
if ( user . Policy . IsDisabled )
{
2019-06-09 22:08:01 +02:00
throw new AuthenticationException (
string . Format (
CultureInfo . InvariantCulture ,
"The {0} account is currently disabled. Please consult with your administrator." ,
user . Name ) ) ;
2019-03-17 05:36:45 +01:00
}
if ( ! user . Policy . EnableRemoteAccess & & ! _networkManager . IsInLocalNetwork ( remoteEndPoint ) )
{
2019-05-21 19:28:34 +02:00
throw new AuthenticationException ( "Forbidden." ) ;
2019-03-17 05:36:45 +01:00
}
if ( ! user . IsParentalScheduleAllowed ( ) )
{
2019-05-21 19:28:34 +02:00
throw new AuthenticationException ( "User is not allowed access at this time." ) ;
2019-03-17 05:36:45 +01:00
}
// Update LastActivityDate and LastLoginDate, then save
if ( success )
{
if ( isUserSession )
{
user . LastActivityDate = user . LastLoginDate = DateTime . UtcNow ;
UpdateUser ( user ) ;
}
2019-05-21 19:28:34 +02:00
2019-09-13 17:16:33 +02:00
ResetInvalidLoginAttemptCount ( user ) ;
2019-03-17 05:36:45 +01:00
}
else
{
2019-09-13 17:16:33 +02:00
IncrementInvalidLoginAttemptCount ( user ) ;
2019-03-17 05:36:45 +01:00
}
_logger . LogInformation ( "Authentication request for {0} {1}." , user . Name , success ? "has succeeded" : "has been denied" ) ;
return success ? user : null ;
}
2019-11-01 18:38:54 +01:00
#nullable enable
2019-03-17 05:36:45 +01:00
private static string GetAuthenticationProviderId ( IAuthenticationProvider provider )
{
return provider . GetType ( ) . FullName ;
}
2019-03-22 08:01:23 +01:00
private static string GetPasswordResetProviderId ( IPasswordResetProvider provider )
{
return provider . GetType ( ) . FullName ;
}
2019-03-17 05:36:45 +01:00
private IAuthenticationProvider GetAuthenticationProvider ( User user )
{
2019-06-09 22:08:01 +02:00
return GetAuthenticationProviders ( user ) [ 0 ] ;
2019-03-17 05:36:45 +01:00
}
2019-03-22 08:01:23 +01:00
private IPasswordResetProvider GetPasswordResetProvider ( User user )
{
2019-03-26 05:40:10 +01:00
return GetPasswordResetProviders ( user ) [ 0 ] ;
2019-03-22 08:01:23 +01:00
}
2019-11-01 18:38:54 +01:00
private IAuthenticationProvider [ ] GetAuthenticationProviders ( User ? user )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
var authenticationProviderId = user ? . Policy . AuthenticationProviderId ;
2019-03-17 05:36:45 +01:00
var providers = _authenticationProviders . Where ( i = > i . IsEnabled ) . ToArray ( ) ;
if ( ! string . IsNullOrEmpty ( authenticationProviderId ) )
{
providers = providers . Where ( i = > string . Equals ( authenticationProviderId , GetAuthenticationProviderId ( i ) , StringComparison . OrdinalIgnoreCase ) ) . ToArray ( ) ;
}
if ( providers . Length = = 0 )
{
2019-06-09 17:07:35 +02:00
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
2019-06-09 19:41:14 +02:00
_logger . LogWarning ( "User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected" , user . Name , user . Policy . AuthenticationProviderId ) ;
2019-06-09 04:54:31 +02:00
providers = new IAuthenticationProvider [ ] { _invalidAuthProvider } ;
2019-03-17 05:36:45 +01:00
}
return providers ;
}
2019-11-01 18:38:54 +01:00
private IPasswordResetProvider [ ] GetPasswordResetProviders ( User ? user )
2019-03-22 08:01:23 +01:00
{
2019-03-24 19:41:03 +01:00
var passwordResetProviderId = user ? . Policy . PasswordResetProviderId ;
2019-03-22 08:01:23 +01:00
var providers = _passwordResetProviders . Where ( i = > i . IsEnabled ) . ToArray ( ) ;
if ( ! string . IsNullOrEmpty ( passwordResetProviderId ) )
{
providers = providers . Where ( i = > string . Equals ( passwordResetProviderId , GetPasswordResetProviderId ( i ) , StringComparison . OrdinalIgnoreCase ) ) . ToArray ( ) ;
}
2019-06-09 21:27:38 +02:00
if ( providers . Length = = 0 )
{
providers = new IPasswordResetProvider [ ] { _defaultPasswordResetProvider } ;
}
2019-03-22 08:01:23 +01:00
return providers ;
}
2019-11-01 18:38:54 +01:00
private async Task < ( string username , bool success ) > AuthenticateWithProvider (
IAuthenticationProvider provider ,
string username ,
string password ,
User ? resolvedUser )
2019-03-17 05:36:45 +01:00
{
try
{
2019-06-09 22:08:01 +02:00
var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
? await requiresResolvedUser . Authenticate ( username , password , resolvedUser ) . ConfigureAwait ( false )
: await provider . Authenticate ( username , password ) . ConfigureAwait ( false ) ;
2019-04-08 01:51:45 +02:00
2019-05-21 19:28:34 +02:00
if ( authenticationResult . Username ! = username )
2019-04-08 01:51:45 +02:00
{
_logger . LogDebug ( "Authentication provider provided updated username {1}" , authenticationResult . Username ) ;
username = authenticationResult . Username ;
2019-03-17 05:36:45 +01:00
}
2019-05-21 19:28:34 +02:00
return ( username , true ) ;
2019-03-17 05:36:45 +01:00
}
2019-05-21 19:28:34 +02:00
catch ( AuthenticationException ex )
2019-03-17 05:36:45 +01:00
{
2019-05-21 19:28:34 +02:00
_logger . LogError ( ex , "Error authenticating with provider {Provider}" , provider . Name ) ;
2019-03-17 05:36:45 +01:00
2019-05-21 19:28:34 +02:00
return ( username , false ) ;
2019-03-17 05:36:45 +01:00
}
}
2019-11-01 18:38:54 +01:00
private async Task < ( IAuthenticationProvider ? authenticationProvider , string username , bool success ) > AuthenticateLocalUser (
2019-09-17 18:07:15 +02:00
string username ,
string password ,
string hashedPassword ,
2019-11-01 18:38:54 +01:00
User ? user ,
2019-09-17 18:07:15 +02:00
string remoteEndPoint )
2019-03-17 05:36:45 +01:00
{
bool success = false ;
2019-11-01 18:38:54 +01:00
IAuthenticationProvider ? authenticationProvider = null ;
2019-03-17 05:36:45 +01:00
2019-09-17 18:07:15 +02:00
foreach ( var provider in GetAuthenticationProviders ( user ) )
2019-03-17 05:36:45 +01:00
{
2019-09-17 18:07:15 +02:00
var providerAuthResult = await AuthenticateWithProvider ( provider , username , password , user ) . ConfigureAwait ( false ) ;
var updatedUsername = providerAuthResult . username ;
success = providerAuthResult . success ;
2019-03-17 05:36:45 +01:00
2019-09-17 18:07:15 +02:00
if ( success )
2019-03-17 05:36:45 +01:00
{
2019-09-17 18:07:15 +02:00
authenticationProvider = provider ;
username = updatedUsername ;
break ;
2019-03-17 05:36:45 +01:00
}
}
2019-09-17 18:07:15 +02:00
if ( ! success
2019-06-09 22:08:01 +02:00
& & _networkManager . IsInLocalNetwork ( remoteEndPoint )
2019-11-21 17:32:29 +01:00
& & user . Configuration . EnableLocalPassword
& & ! string . IsNullOrEmpty ( user . EasyPassword ) )
2019-03-17 05:36:45 +01:00
{
2019-11-21 17:32:29 +01:00
// Check easy password
var passwordHash = PasswordHash . Parse ( user . EasyPassword ) ;
var hash = _cryptoProvider . ComputeHash (
passwordHash . Id ,
Encoding . UTF8 . GetBytes ( password ) ,
passwordHash . Salt ) ;
success = passwordHash . Hash . SequenceEqual ( hash ) ;
2019-03-17 05:36:45 +01:00
}
2019-05-21 19:28:34 +02:00
return ( authenticationProvider , username , success ) ;
2019-03-17 05:36:45 +01:00
}
2019-09-13 17:16:33 +02:00
private void ResetInvalidLoginAttemptCount ( User user )
2019-03-17 05:36:45 +01:00
{
2019-09-13 17:16:33 +02:00
user . Policy . InvalidLoginAttemptCount = 0 ;
UpdateUserPolicy ( user , user . Policy , false ) ;
}
2019-03-17 05:36:45 +01:00
2019-09-13 17:16:33 +02:00
private void IncrementInvalidLoginAttemptCount ( User user )
{
int invalidLogins = + + user . Policy . InvalidLoginAttemptCount ;
int maxInvalidLogins = user . Policy . LoginAttemptsBeforeLockout ;
if ( maxInvalidLogins > 0
& & invalidLogins > = maxInvalidLogins )
2019-03-17 05:36:45 +01:00
{
user . Policy . IsDisabled = true ;
2019-09-13 17:16:33 +02:00
UserLockedOut ? . Invoke ( this , new GenericEventArgs < User > ( user ) ) ;
_logger . LogWarning (
"Disabling user {UserName} due to {Attempts} unsuccessful login attempts." ,
user . Name ,
invalidLogins ) ;
2019-03-17 05:36:45 +01:00
}
UpdateUserPolicy ( user , user . Policy , false ) ;
}
/// <summary>
2019-06-09 22:08:01 +02:00
/// Loads the users from the repository.
2019-03-17 05:36:45 +01:00
/// </summary>
2019-06-09 22:08:01 +02:00
private void LoadUsers ( )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
var users = _userRepository . RetrieveAllUsers ( ) ;
2019-03-17 05:36:45 +01:00
// There always has to be at least one user.
2019-04-03 18:15:04 +02:00
if ( users . Count ! = 0 )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
_users = new ConcurrentDictionary < Guid , User > (
users . Select ( x = > new KeyValuePair < Guid , User > ( x . Id , x ) ) ) ;
2019-08-18 20:12:25 +02:00
return ;
2019-04-03 18:15:04 +02:00
}
2019-03-17 05:36:45 +01:00
2019-04-03 18:15:04 +02:00
var defaultName = Environment . UserName ;
if ( string . IsNullOrWhiteSpace ( defaultName ) )
{
defaultName = "MyJellyfinUser" ;
}
2019-03-17 05:36:45 +01:00
2019-10-20 21:12:03 +02:00
_logger . LogWarning ( "No users, creating one with username {UserName}" , defaultName ) ;
2019-04-03 18:15:04 +02:00
var name = MakeValidUsername ( defaultName ) ;
2019-03-17 05:36:45 +01:00
2019-04-03 18:15:04 +02:00
var user = InstantiateNewUser ( name ) ;
2019-03-17 05:36:45 +01:00
2019-04-03 18:15:04 +02:00
user . DateLastSaved = DateTime . UtcNow ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
_userRepository . CreateUser ( user ) ;
2019-04-03 18:15:04 +02:00
user . Policy . IsAdministrator = true ;
user . Policy . EnableContentDeletion = true ;
user . Policy . EnableRemoteControlOfOtherUsers = true ;
UpdateUserPolicy ( user , user . Policy , false ) ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
_users = new ConcurrentDictionary < Guid , User > ( ) ;
_users [ user . Id ] = user ;
2019-03-17 05:36:45 +01:00
}
2019-11-01 18:38:54 +01:00
#nullable restore
2019-03-17 05:36:45 +01:00
public UserDto GetUserDto ( User user , string remoteEndPoint = null )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2019-05-21 19:28:34 +02:00
bool hasConfiguredPassword = GetAuthenticationProvider ( user ) . HasPassword ( user ) ;
2019-05-25 19:46:55 +02:00
bool hasConfiguredEasyPassword = ! string . IsNullOrEmpty ( GetAuthenticationProvider ( user ) . GetEasyPasswordHash ( user ) ) ;
2019-03-17 05:36:45 +01:00
bool hasPassword = user . Configuration . EnableLocalPassword & & ! string . IsNullOrEmpty ( remoteEndPoint ) & & _networkManager . IsInLocalNetwork ( remoteEndPoint ) ?
hasConfiguredEasyPassword :
hasConfiguredPassword ;
UserDto dto = new UserDto
{
Id = user . Id ,
Name = user . Name ,
HasPassword = hasPassword ,
HasConfiguredPassword = hasConfiguredPassword ,
HasConfiguredEasyPassword = hasConfiguredEasyPassword ,
LastActivityDate = user . LastActivityDate ,
LastLoginDate = user . LastLoginDate ,
Configuration = user . Configuration ,
ServerId = _appHost . SystemId ,
Policy = user . Policy
} ;
2019-06-09 22:08:01 +02:00
if ( ! hasPassword & & _users . Count = = 1 )
2019-03-17 05:36:45 +01:00
{
dto . EnableAutoLogin = true ;
}
ItemImageInfo image = user . GetImageInfo ( ImageType . Primary , 0 ) ;
if ( image ! = null )
{
dto . PrimaryImageTag = GetImageCacheTag ( user , image ) ;
try
{
_dtoServiceFactory ( ) . AttachPrimaryImageAspectRatio ( dto , user ) ;
}
catch ( Exception ex )
{
// Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
2019-10-20 21:12:03 +02:00
_logger . LogError ( ex , "Error generating PrimaryImageAspectRatio for {User}" , user . Name ) ;
2019-03-17 05:36:45 +01:00
}
}
return dto ;
}
public UserDto GetOfflineUserDto ( User user )
{
var dto = GetUserDto ( user ) ;
dto . ServerName = _appHost . FriendlyName ;
return dto ;
}
private string GetImageCacheTag ( BaseItem item , ItemImageInfo image )
{
try
{
return _imageProcessorFactory ( ) . GetImageCacheTag ( item , image ) ;
}
catch ( Exception ex )
{
2019-10-20 21:12:03 +02:00
_logger . LogError ( ex , "Error getting {ImageType} image info for {ImagePath}" , image . Type , image . Path ) ;
2019-03-17 05:36:45 +01:00
return null ;
}
}
/// <summary>
/// Refreshes metadata for each user
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task RefreshUsersMetadata ( CancellationToken cancellationToken )
{
foreach ( var user in Users )
{
2019-09-10 22:37:53 +02:00
await user . RefreshMetadata ( new MetadataRefreshOptions ( new DirectoryService ( _fileSystem ) ) , cancellationToken ) . ConfigureAwait ( false ) ;
2019-03-17 05:36:45 +01:00
}
}
/// <summary>
/// Renames the user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">user</exception>
/// <exception cref="ArgumentException"></exception>
public async Task RenameUser ( User user , string newName )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2019-06-09 22:08:01 +02:00
if ( string . IsNullOrWhiteSpace ( newName ) )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( "Invalid username" , nameof ( newName ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
if ( user . Name . Equals ( newName , StringComparison . OrdinalIgnoreCase ) )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( "The new and old names must be different." ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
if ( Users . Any (
u = > u . Id ! = user . Id & & u . Name . Equals ( newName , StringComparison . OrdinalIgnoreCase ) ) )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( string . Format (
CultureInfo . InvariantCulture ,
"A user with the name '{0}' already exists." ,
newName ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
await user . Rename ( newName ) . ConfigureAwait ( false ) ;
2019-03-17 05:36:45 +01:00
OnUserUpdated ( user ) ;
}
/// <summary>
/// Updates the user.
/// </summary>
/// <param name="user">The user.</param>
/// <exception cref="ArgumentNullException">user</exception>
/// <exception cref="ArgumentException"></exception>
public void UpdateUser ( User user )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2019-06-09 22:08:01 +02:00
if ( user . Id = = Guid . Empty )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( "Id can't be empty." , nameof ( user ) ) ;
}
if ( ! _users . ContainsKey ( user . Id ) )
{
throw new ArgumentException (
string . Format (
CultureInfo . InvariantCulture ,
"A user '{0}' with Id {1} does not exist." ,
user . Name ,
user . Id ) ,
nameof ( user ) ) ;
2019-03-17 05:36:45 +01:00
}
user . DateModified = DateTime . UtcNow ;
user . DateLastSaved = DateTime . UtcNow ;
2019-06-09 22:08:01 +02:00
_userRepository . UpdateUser ( user ) ;
2019-03-17 05:36:45 +01:00
OnUserUpdated ( user ) ;
}
/// <summary>
/// Creates the user.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>User.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
2019-06-09 22:08:01 +02:00
public User CreateUser ( string name )
2019-03-17 05:36:45 +01:00
{
if ( string . IsNullOrWhiteSpace ( name ) )
{
throw new ArgumentNullException ( nameof ( name ) ) ;
}
if ( ! IsValidUsername ( name ) )
{
throw new ArgumentException ( "Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)" ) ;
}
if ( Users . Any ( u = > u . Name . Equals ( name , StringComparison . OrdinalIgnoreCase ) ) )
{
throw new ArgumentException ( string . Format ( "A user with the name '{0}' already exists." , name ) ) ;
}
2019-06-09 22:08:01 +02:00
var user = InstantiateNewUser ( name ) ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
_users [ user . Id ] = user ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
user . DateLastSaved = DateTime . UtcNow ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
_userRepository . CreateUser ( user ) ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
EventHelper . QueueEventIfNotNull ( UserCreated , this , new GenericEventArgs < User > { Argument = user } , _logger ) ;
2019-03-17 05:36:45 +01:00
2019-06-09 22:08:01 +02:00
return user ;
2019-03-17 05:36:45 +01:00
}
/// <summary>
/// Deletes the user.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">user</exception>
/// <exception cref="ArgumentException"></exception>
2019-06-09 22:08:01 +02:00
public void DeleteUser ( User user )
2019-03-17 05:36:45 +01:00
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2019-06-09 22:08:01 +02:00
if ( ! _users . ContainsKey ( user . Id ) )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( string . Format (
CultureInfo . InvariantCulture ,
"The user cannot be deleted because there is no user with the Name {0} and Id {1}." ,
user . Name ,
user . Id ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
if ( _users . Count = = 1 )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException ( string . Format (
CultureInfo . InvariantCulture ,
"The user '{0}' cannot be deleted because there must be at least one user in the system." ,
user . Name ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
if ( user . Policy . IsAdministrator
& & Users . Count ( i = > i . Policy . IsAdministrator ) = = 1 )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
throw new ArgumentException (
string . Format (
CultureInfo . InvariantCulture ,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system." ,
user . Name ) ,
nameof ( user ) ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
var configPath = GetConfigurationFilePath ( user ) ;
_userRepository . DeleteUser ( user ) ;
2019-03-17 05:36:45 +01:00
try
{
2019-06-09 22:08:01 +02:00
_fileSystem . DeleteFile ( configPath ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
catch ( IOException ex )
2019-03-17 05:36:45 +01:00
{
2019-06-09 22:08:01 +02:00
_logger . LogError ( ex , "Error deleting file {path}" , configPath ) ;
2019-03-17 05:36:45 +01:00
}
2019-06-09 22:08:01 +02:00
DeleteUserPolicy ( user ) ;
_users . TryRemove ( user . Id , out _ ) ;
OnUserDeleted ( user ) ;
2019-03-17 05:36:45 +01:00
}
/// <summary>
/// Resets the password by clearing it.
/// </summary>
/// <returns>Task.</returns>
public Task ResetPassword ( User user )
{
return ChangePassword ( user , string . Empty ) ;
}
public void ResetEasyPassword ( User user )
{
ChangeEasyPassword ( user , string . Empty , null ) ;
}
public async Task ChangePassword ( User user , string newPassword )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
await GetAuthenticationProvider ( user ) . ChangePassword ( user , newPassword ) . ConfigureAwait ( false ) ;
UpdateUser ( user ) ;
UserPasswordChanged ? . Invoke ( this , new GenericEventArgs < User > ( user ) ) ;
}
public void ChangeEasyPassword ( User user , string newPassword , string newPasswordHash )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2019-05-25 19:46:55 +02:00
GetAuthenticationProvider ( user ) . ChangeEasyPassword ( user , newPassword , newPasswordHash ) ;
2019-03-17 05:36:45 +01:00
UpdateUser ( user ) ;
UserPasswordChanged ? . Invoke ( this , new GenericEventArgs < User > ( user ) ) ;
}
/// <summary>
/// Instantiates the new user.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>User.</returns>
private static User InstantiateNewUser ( string name )
{
return new User
{
Name = name ,
Id = Guid . NewGuid ( ) ,
DateCreated = DateTime . UtcNow ,
2019-06-09 22:08:01 +02:00
DateModified = DateTime . UtcNow
2019-03-17 05:36:45 +01:00
} ;
}
public async Task < ForgotPasswordResult > StartForgotPasswordProcess ( string enteredUsername , bool isInNetwork )
{
var user = string . IsNullOrWhiteSpace ( enteredUsername ) ?
null :
GetUserByName ( enteredUsername ) ;
var action = ForgotPasswordAction . InNetworkRequired ;
2019-03-22 08:01:23 +01:00
if ( user ! = null & & isInNetwork )
2019-03-17 05:36:45 +01:00
{
2019-03-22 08:01:23 +01:00
var passwordResetProvider = GetPasswordResetProvider ( user ) ;
return await passwordResetProvider . StartForgotPasswordProcess ( user , isInNetwork ) . ConfigureAwait ( false ) ;
2019-03-17 05:36:45 +01:00
}
else
{
2019-03-22 08:01:23 +01:00
return new ForgotPasswordResult
2019-03-17 05:36:45 +01:00
{
2019-03-22 08:01:23 +01:00
Action = action ,
PinFile = string . Empty
} ;
2019-03-17 05:36:45 +01:00
}
}
public async Task < PinRedeemResult > RedeemPasswordResetPin ( string pin )
{
2019-03-22 08:01:23 +01:00
foreach ( var provider in _passwordResetProviders )
2019-03-17 05:36:45 +01:00
{
2019-03-22 08:01:23 +01:00
var result = await provider . RedeemPasswordResetPin ( pin ) . ConfigureAwait ( false ) ;
if ( result . Success )
2019-03-17 05:36:45 +01:00
{
2019-03-22 08:01:23 +01:00
return result ;
2019-03-17 05:36:45 +01:00
}
}
return new PinRedeemResult
{
2019-03-22 08:01:23 +01:00
Success = false ,
UsersReset = Array . Empty < string > ( )
2019-03-17 05:36:45 +01:00
} ;
}
public UserPolicy GetUserPolicy ( User user )
{
var path = GetPolicyFilePath ( user ) ;
if ( ! File . Exists ( path ) )
{
return GetDefaultPolicy ( user ) ;
}
try
{
lock ( _policySyncLock )
{
return ( UserPolicy ) _xmlSerializer . DeserializeFromFile ( typeof ( UserPolicy ) , path ) ;
}
}
catch ( IOException )
{
return GetDefaultPolicy ( user ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error reading policy file: {path}" , path ) ;
return GetDefaultPolicy ( user ) ;
}
}
private static UserPolicy GetDefaultPolicy ( User user )
{
return new UserPolicy
{
EnableContentDownloading = true ,
EnableSyncTranscoding = true
} ;
}
public void UpdateUserPolicy ( Guid userId , UserPolicy userPolicy )
{
var user = GetUserById ( userId ) ;
UpdateUserPolicy ( user , userPolicy , true ) ;
}
private void UpdateUserPolicy ( User user , UserPolicy userPolicy , bool fireEvent )
{
// The xml serializer will output differently if the type is not exact
if ( userPolicy . GetType ( ) ! = typeof ( UserPolicy ) )
{
var json = _jsonSerializer . SerializeToString ( userPolicy ) ;
userPolicy = _jsonSerializer . DeserializeFromString < UserPolicy > ( json ) ;
}
var path = GetPolicyFilePath ( user ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ) ;
lock ( _policySyncLock )
{
_xmlSerializer . SerializeToFile ( userPolicy , path ) ;
user . Policy = userPolicy ;
}
if ( fireEvent )
{
UserPolicyUpdated ? . Invoke ( this , new GenericEventArgs < User > { Argument = user } ) ;
}
}
private void DeleteUserPolicy ( User user )
{
var path = GetPolicyFilePath ( user ) ;
try
{
lock ( _policySyncLock )
{
_fileSystem . DeleteFile ( path ) ;
}
}
catch ( IOException )
{
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error deleting policy file" ) ;
}
}
private static string GetPolicyFilePath ( User user )
{
return Path . Combine ( user . ConfigurationDirectoryPath , "policy.xml" ) ;
}
private static string GetConfigurationFilePath ( User user )
{
return Path . Combine ( user . ConfigurationDirectoryPath , "config.xml" ) ;
}
public UserConfiguration GetUserConfiguration ( User user )
{
var path = GetConfigurationFilePath ( user ) ;
if ( ! File . Exists ( path ) )
{
return new UserConfiguration ( ) ;
}
try
{
lock ( _configSyncLock )
{
return ( UserConfiguration ) _xmlSerializer . DeserializeFromFile ( typeof ( UserConfiguration ) , path ) ;
}
}
catch ( IOException )
{
return new UserConfiguration ( ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error reading policy file: {path}" , path ) ;
return new UserConfiguration ( ) ;
}
}
private readonly object _configSyncLock = new object ( ) ;
public void UpdateConfiguration ( Guid userId , UserConfiguration config )
{
var user = GetUserById ( userId ) ;
UpdateConfiguration ( user , config ) ;
}
public void UpdateConfiguration ( User user , UserConfiguration config )
{
UpdateConfiguration ( user , config , true ) ;
}
private void UpdateConfiguration ( User user , UserConfiguration config , bool fireEvent )
{
var path = GetConfigurationFilePath ( user ) ;
// The xml serializer will output differently if the type is not exact
if ( config . GetType ( ) ! = typeof ( UserConfiguration ) )
{
var json = _jsonSerializer . SerializeToString ( config ) ;
config = _jsonSerializer . DeserializeFromString < UserConfiguration > ( json ) ;
}
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ) ;
lock ( _configSyncLock )
{
_xmlSerializer . SerializeToFile ( config , path ) ;
user . Configuration = config ;
}
if ( fireEvent )
{
UserConfigurationUpdated ? . Invoke ( this , new GenericEventArgs < User > { Argument = user } ) ;
}
}
}
public class DeviceAccessEntryPoint : IServerEntryPoint
{
private IUserManager _userManager ;
private IAuthenticationRepository _authRepo ;
private IDeviceManager _deviceManager ;
private ISessionManager _sessionManager ;
public DeviceAccessEntryPoint ( IUserManager userManager , IAuthenticationRepository authRepo , IDeviceManager deviceManager , ISessionManager sessionManager )
{
_userManager = userManager ;
_authRepo = authRepo ;
_deviceManager = deviceManager ;
_sessionManager = sessionManager ;
}
public Task RunAsync ( )
{
_userManager . UserPolicyUpdated + = _userManager_UserPolicyUpdated ;
return Task . CompletedTask ;
}
private void _userManager_UserPolicyUpdated ( object sender , GenericEventArgs < User > e )
{
var user = e . Argument ;
if ( ! user . Policy . EnableAllDevices )
{
UpdateDeviceAccess ( user ) ;
}
}
private void UpdateDeviceAccess ( User user )
{
var existing = _authRepo . Get ( new AuthenticationInfoQuery
{
UserId = user . Id
} ) . Items ;
foreach ( var authInfo in existing )
{
if ( ! string . IsNullOrEmpty ( authInfo . DeviceId ) & & ! _deviceManager . CanAccessDevice ( user , authInfo . DeviceId ) )
{
_sessionManager . Logout ( authInfo ) ;
}
}
}
public void Dispose ( )
{
}
}
}