diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/Library/UserManager.cs')
| -rw-r--r-- | MediaBrowser.Server.Implementations/Library/UserManager.cs | 528 |
1 files changed, 490 insertions, 38 deletions
diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index 16a1dc516..503af4970 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -1,22 +1,28 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Connect; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; -using MediaBrowser.Server.Implementations.Security; +using MediaBrowser.Model.Users; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -56,19 +62,16 @@ namespace MediaBrowser.Server.Implementations.Library public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; private readonly INetworkManager _networkManager; private readonly Func<IImageProcessor> _imageProcessorFactory; private readonly Func<IDtoService> _dtoServiceFactory; + private readonly Func<IConnectManager> _connectFactory; + private readonly IServerApplicationHost _appHost; - /// <summary> - /// Initializes a new instance of the <see cref="UserManager" /> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="configurationManager">The configuration manager.</param> - /// <param name="userRepository">The user repository.</param> - public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory) + public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer) { _logger = logger; UserRepository = userRepository; @@ -76,8 +79,13 @@ namespace MediaBrowser.Server.Implementations.Library _networkManager = networkManager; _imageProcessorFactory = imageProcessorFactory; _dtoServiceFactory = dtoServiceFactory; + _connectFactory = connectFactory; + _appHost = appHost; + _jsonSerializer = jsonSerializer; ConfigurationManager = configurationManager; Users = new List<User>(); + + DeletePinFile(); } #region UserUpdated Event @@ -138,30 +146,94 @@ namespace MediaBrowser.Server.Implementations.Library return GetUserById(new Guid(id)); } + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + } + public async Task Initialize() { Users = await LoadUsers().ConfigureAwait(false); + + foreach (var user in Users.ToList()) + { + await DoPolicyMigration(user).ConfigureAwait(false); + } } - public async Task<bool> AuthenticateUser(string username, string password, string remoteEndPoint) + public Task<bool> AuthenticateUser(string username, string passwordSha1, string remoteEndPoint) + { + return AuthenticateUser(username, passwordSha1, null, remoteEndPoint); + } + + public bool IsValidUsername(string username) + { + // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + return username.All(IsValidCharacter); + } + + private bool IsValidCharacter(char i) + { + return char.IsLetterOrDigit(i) || char.Equals(i, '-') || char.Equals(i, '_') || char.Equals(i, '\'') || + char.Equals(i, '.'); + } + + 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 (IsValidCharacter(c)) + { + builder.Append(c); + } + } + return builder.ToString(); + } + + public async Task<bool> AuthenticateUser(string username, string passwordSha1, string passwordMd5, string remoteEndPoint) { if (string.IsNullOrWhiteSpace(username)) { throw new ArgumentNullException("username"); } - var user = Users.First(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + var user = Users + .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + + if (user == null) + { + throw new SecurityException("Invalid username or password entered."); + } - if (user.Configuration.IsDisabled) + if (user.Policy.IsDisabled) { - throw new AuthenticationException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); } - var success = string.Equals(GetPasswordHash(user), password.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + var success = false; - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + // Authenticate using local credentials if not a guest + if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value != UserLinkType.Guest) { - success = string.Equals(GetLocalPasswordHash(user), password.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + success = string.Equals(GetPasswordHash(user), passwordSha1.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + success = string.Equals(GetLocalPasswordHash(user), passwordSha1.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } } // Update LastActivityDate and LastLoginDate, then save @@ -220,9 +292,9 @@ namespace MediaBrowser.Server.Implementations.Library // There always has to be at least one user. if (users.Count == 0) { - var name = Environment.UserName; + var name = MakeValidUsername(Environment.UserName); - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name, false); user.DateLastSaved = DateTime.UtcNow; @@ -230,13 +302,42 @@ namespace MediaBrowser.Server.Implementations.Library users.Add(user); - user.Configuration.IsAdministrator = true; - UpdateConfiguration(user, user.Configuration); + user.Policy.IsAdministrator = true; + user.Policy.EnableRemoteControlOfOtherUsers = true; + await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false); } return users; } + private async Task DoPolicyMigration(User user) + { + if (!user.Configuration.HasMigratedToPolicy) + { + user.Policy.AccessSchedules = user.Configuration.AccessSchedules; + user.Policy.BlockedChannels = user.Configuration.BlockedChannels; + user.Policy.BlockedMediaFolders = user.Configuration.BlockedMediaFolders; + user.Policy.BlockedTags = user.Configuration.BlockedTags; + user.Policy.BlockUnratedItems = user.Configuration.BlockUnratedItems; + user.Policy.EnableContentDeletion = user.Configuration.EnableContentDeletion; + user.Policy.EnableLiveTvAccess = user.Configuration.EnableLiveTvAccess; + user.Policy.EnableLiveTvManagement = user.Configuration.EnableLiveTvManagement; + user.Policy.EnableMediaPlayback = user.Configuration.EnableMediaPlayback; + user.Policy.EnableRemoteControlOfOtherUsers = user.Configuration.EnableRemoteControlOfOtherUsers; + user.Policy.EnableSharedDeviceControl = user.Configuration.EnableSharedDeviceControl; + user.Policy.EnableUserPreferenceAccess = user.Configuration.EnableUserPreferenceAccess; + user.Policy.IsAdministrator = user.Configuration.IsAdministrator; + user.Policy.IsDisabled = user.Configuration.IsDisabled; + user.Policy.IsHidden = user.Configuration.IsHidden; + user.Policy.MaxParentalRating = user.Configuration.MaxParentalRating; + + await UpdateUserPolicy(user.Id.ToString("N"), user.Policy); + + user.Configuration.HasMigratedToPolicy = true; + await UpdateConfiguration(user, user.Configuration, true).ConfigureAwait(false); + } + } + public UserDto GetUserDto(User user, string remoteEndPoint = null) { if (user == null) @@ -263,7 +364,9 @@ namespace MediaBrowser.Server.Implementations.Library Configuration = user.Configuration, ConnectLinkType = user.ConnectLinkType, ConnectUserId = user.ConnectUserId, - ConnectUserName = user.ConnectUserName + ConnectUserName = user.ConnectUserName, + ServerId = _appHost.SystemId, + Policy = user.Policy }; var image = user.GetImageInfo(ImageType.Primary, 0); @@ -274,7 +377,10 @@ namespace MediaBrowser.Server.Implementations.Library try { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user, new List<ItemFields> + { + ItemFields.PrimaryImageAspectRatio + }); } catch (Exception ex) { @@ -390,6 +496,11 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("name"); } + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain letters (a-z), 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)); @@ -399,7 +510,7 @@ namespace MediaBrowser.Server.Implementations.Library try { - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name, true); var list = Users.ToList(); list.Add(user); @@ -433,6 +544,11 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("user"); } + if (user.ConnectLinkType.HasValue) + { + await _connectFactory().RemoveConnect(user.Id.ToString("N")).ConfigureAwait(false); + } + var allUsers = Users.ToList(); if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) @@ -445,7 +561,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); } - if (user.Configuration.IsAdministrator && allUsers.Count(i => i.Configuration.IsAdministrator) == 1) + if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) { throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); } @@ -454,19 +570,21 @@ namespace MediaBrowser.Server.Implementations.Library try { - await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); + var configPath = GetConfigurationFilePath(user); - var path = user.ConfigurationFilePath; + await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); try { - File.Delete(path); + File.Delete(configPath); } catch (IOException ex) { - _logger.ErrorException("Error deleting file {0}", ex, path); + _logger.ErrorException("Error deleting file {0}", ex, configPath); } + DeleteUserPolicy(user); + // Force this to be lazy loaded again Users = await LoadUsers().ConfigureAwait(false); @@ -484,23 +602,38 @@ namespace MediaBrowser.Server.Implementations.Library /// <returns>Task.</returns> public Task ResetPassword(User user) { - return ChangePassword(user, string.Empty); + return ChangePassword(user, GetSha1String(string.Empty)); } /// <summary> /// Changes the password. /// </summary> /// <param name="user">The user.</param> - /// <param name="newPassword">The new password.</param> + /// <param name="newPasswordSha1">The new password sha1.</param> /// <returns>Task.</returns> - public async Task ChangePassword(User user, string newPassword) + /// <exception cref="System.ArgumentNullException"> + /// user + /// or + /// newPassword + /// </exception> + /// <exception cref="System.ArgumentException">Passwords for guests cannot be changed.</exception> + public async Task ChangePassword(User user, string newPasswordSha1) { if (user == null) { throw new ArgumentNullException("user"); } + if (string.IsNullOrWhiteSpace(newPasswordSha1)) + { + throw new ArgumentNullException("newPasswordSha1"); + } - user.Password = string.IsNullOrEmpty(newPassword) ? GetSha1String(string.Empty) : GetSha1String(newPassword); + if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) + { + throw new ArgumentException("Passwords for guests cannot be changed."); + } + + user.Password = newPasswordSha1; await UpdateUser(user).ConfigureAwait(false); @@ -511,26 +644,345 @@ namespace MediaBrowser.Server.Implementations.Library /// Instantiates the new user. /// </summary> /// <param name="name">The name.</param> + /// <param name="checkId">if set to <c>true</c> [check identifier].</param> /// <returns>User.</returns> - private User InstantiateNewUser(string name) + private User InstantiateNewUser(string name, bool checkId) { + var id = ("MBUser" + name).GetMD5(); + + if (checkId && Users.Select(i => i.Id).Contains(id)) + { + id = Guid.NewGuid(); + } + return new User { Name = name, - Id = ("MBUser" + name).GetMD5(), + Id = id, DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, UsesIdForConfigurationPath = true }; } - public void UpdateConfiguration(User user, UserConfiguration newConfiguration) + private string PasswordResetFile + { + get { return Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt"); } + } + + private string _lastPin; + private PasswordPinCreationResult _lastPasswordPinCreationResult; + private int _pinAttempts; + + private PasswordPinCreationResult CreatePasswordResetPin() + { + var num = new Random().Next(1, 9999); + + var path = PasswordResetFile; + + var pin = num.ToString("0000", CultureInfo.InvariantCulture); + _lastPin = pin; + + var time = TimeSpan.FromMinutes(5); + var expiration = DateTime.UtcNow.Add(time); + + var text = new StringBuilder(); + + var info = _appHost.GetSystemInfo(); + var localAddress = info.LocalAddress ?? string.Empty; + + text.AppendLine("Use your web browser to visit:"); + text.AppendLine(string.Empty); + text.AppendLine(localAddress + "/mediabrowser/web/forgotpasswordpin.html"); + text.AppendLine(string.Empty); + text.AppendLine("Enter the following pin code:"); + text.AppendLine(string.Empty); + text.AppendLine(pin); + text.AppendLine(string.Empty); + text.AppendLine("The pin code will expire at " + expiration.ToLocalTime().ToShortDateString() + " " + expiration.ToLocalTime().ToShortTimeString()); + + File.WriteAllText(path, text.ToString(), Encoding.UTF8); + + var result = new PasswordPinCreationResult + { + PinFile = path, + ExpirationDate = expiration + }; + + _lastPasswordPinCreationResult = result; + _pinAttempts = 0; + + return result; + } + + public ForgotPasswordResult StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { - var xmlPath = user.ConfigurationFilePath; - Directory.CreateDirectory(Path.GetDirectoryName(xmlPath)); - _xmlSerializer.SerializeToFile(newConfiguration, xmlPath); + DeletePinFile(); - EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + var user = string.IsNullOrWhiteSpace(enteredUsername) ? + null : + GetUserByName(enteredUsername); + + if (user != null && user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) + { + throw new ArgumentException("Unable to process forgot password request for guests."); + } + + var action = ForgotPasswordAction.InNetworkRequired; + string pinFile = null; + DateTime? expirationDate = null; + + if (user != null && !user.Policy.IsAdministrator) + { + action = ForgotPasswordAction.ContactAdmin; + } + else + { + if (isInNetwork) + { + action = ForgotPasswordAction.PinCode; + } + + var result = CreatePasswordResetPin(); + pinFile = result.PinFile; + expirationDate = result.ExpirationDate; + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = pinFile, + PinExpirationDate = expirationDate + }; + } + + public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) + { + DeletePinFile(); + + var usersReset = new List<string>(); + + var valid = !string.IsNullOrWhiteSpace(_lastPin) && + string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) && + _lastPasswordPinCreationResult != null && + _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow; + + if (valid) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + + var users = Users.Where(i => !i.ConnectLinkType.HasValue || i.ConnectLinkType.Value != UserLinkType.Guest) + .ToList(); + + foreach (var user in users) + { + await ResetPassword(user).ConfigureAwait(false); + usersReset.Add(user.Name); + } + } + else + { + _pinAttempts++; + if (_pinAttempts >= 3) + { + _lastPin = null; + _lastPasswordPinCreationResult = null; + } + } + + return new PinRedeemResult + { + Success = valid, + UsersReset = usersReset.ToArray() + }; + } + + private void DeletePinFile() + { + try + { + File.Delete(PasswordResetFile); + } + catch + { + + } + } + + class PasswordPinCreationResult + { + public string PinFile { get; set; } + public DateTime ExpirationDate { get; set; } + } + + public UserPolicy GetUserPolicy(User user) + { + var path = GetPolifyFilePath(user); + + try + { + lock (_policySyncLock) + { + return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path); + } + } + catch (DirectoryNotFoundException) + { + return GetDefaultPolicy(user); + } + catch (FileNotFoundException) + { + return GetDefaultPolicy(user); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading policy file: {0}", ex, path); + + return GetDefaultPolicy(user); + } + } + + private UserPolicy GetDefaultPolicy(User user) + { + return new UserPolicy + { + EnableSync = true + }; + } + + private readonly object _policySyncLock = new object(); + public Task UpdateUserPolicy(string userId, UserPolicy userPolicy) + { + var user = GetUserById(userId); + return UpdateUserPolicy(user, userPolicy, true); + } + + private async Task 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 updateConfig = user.Policy.IsAdministrator != userPolicy.IsAdministrator || + user.Policy.EnableLiveTvManagement != userPolicy.EnableLiveTvManagement || + user.Policy.EnableLiveTvAccess != userPolicy.EnableLiveTvAccess || + user.Policy.EnableMediaPlayback != userPolicy.EnableMediaPlayback || + user.Policy.EnableContentDeletion != userPolicy.EnableContentDeletion; + + var path = GetPolifyFilePath(user); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_policySyncLock) + { + _xmlSerializer.SerializeToFile(userPolicy, path); + user.Policy = userPolicy; + } + + if (updateConfig) + { + user.Configuration.IsAdministrator = user.Policy.IsAdministrator; + user.Configuration.EnableLiveTvManagement = user.Policy.EnableLiveTvManagement; + user.Configuration.EnableLiveTvAccess = user.Policy.EnableLiveTvAccess; + user.Configuration.EnableMediaPlayback = user.Policy.EnableMediaPlayback; + user.Configuration.EnableContentDeletion = user.Policy.EnableContentDeletion; + + await UpdateConfiguration(user, user.Configuration, true).ConfigureAwait(false); + } + } + + private void DeleteUserPolicy(User user) + { + var path = GetPolifyFilePath(user); + + try + { + lock (_policySyncLock) + { + File.Delete(path); + } + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting policy file", ex); + } + } + + private string GetPolifyFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); + } + + private string GetConfigurationFilePath(User user) + { + return Path.Combine(user.ConfigurationDirectoryPath, "config.xml"); + } + + public UserConfiguration GetUserConfiguration(User user) + { + var path = GetConfigurationFilePath(user); + + try + { + lock (_configSyncLock) + { + return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path); + } + } + catch (DirectoryNotFoundException) + { + return new UserConfiguration(); + } + catch (FileNotFoundException) + { + return new UserConfiguration(); + } + catch (Exception ex) + { + _logger.ErrorException("Error reading policy file: {0}", ex, path); + + return new UserConfiguration(); + } + } + + private readonly object _configSyncLock = new object(); + public Task UpdateConfiguration(string userId, UserConfiguration config) + { + var user = GetUserById(userId); + return UpdateConfiguration(user, config, true); + } + + private async Task 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) + { + EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + } } } } |
