using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Library { /// /// Class UserManager /// public class UserManager : IUserManager { /// /// Gets the users. /// /// The users. public IEnumerable Users { get; private set; } /// /// The _logger /// private readonly ILogger _logger; /// /// Gets or sets the configuration manager. /// /// The configuration manager. private IServerConfigurationManager ConfigurationManager { get; set; } /// /// Gets the active user repository /// /// The user repository. private IUserRepository UserRepository { get; set; } private readonly IXmlSerializer _xmlSerializer; /// /// Initializes a new instance of the class. /// /// The logger. /// The configuration manager. /// The user repository. public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer) { _logger = logger; UserRepository = userRepository; _xmlSerializer = xmlSerializer; ConfigurationManager = configurationManager; Users = new List(); } #region UserUpdated Event /// /// Occurs when [user updated]. /// public event EventHandler> UserUpdated; public event EventHandler> UserConfigurationUpdated; /// /// Called when [user updated]. /// /// The user. private void OnUserUpdated(User user) { EventHelper.FireEventIfNotNull(UserUpdated, this, new GenericEventArgs { Argument = user }, _logger); } #endregion #region UserDeleted Event /// /// Occurs when [user deleted]. /// public event EventHandler> UserDeleted; /// /// Called when [user deleted]. /// /// The user. private void OnUserDeleted(User user) { EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs { Argument = user }, _logger); } #endregion /// /// Gets a User by Id /// /// The id. /// User. /// public User GetUserById(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } return Users.FirstOrDefault(u => u.Id == id); } public async Task Initialize() { Users = await LoadUsers().ConfigureAwait(false); } /// /// Authenticates a User and returns a result indicating whether or not it succeeded /// /// The username. /// The password. /// Task{System.Boolean}. /// user /// public async Task AuthenticateUser(string username, string password) { if (string.IsNullOrWhiteSpace(username)) { throw new ArgumentNullException("username"); } var user = Users.First(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); if (user.Configuration.IsDisabled) { throw new UnauthorizedAccessException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); } var existingPasswordString = string.IsNullOrEmpty(user.Password) ? GetSha1String(string.Empty) : user.Password; var success = string.Equals(existingPasswordString, password.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); // Update LastActivityDate and LastLoginDate, then save if (success) { user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; await UpdateUser(user).ConfigureAwait(false); } _logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied")); return success; } /// /// Gets the sha1 string. /// /// The STR. /// System.String. private static string GetSha1String(string str) { using (var provider = SHA1.Create()) { var hash = provider.ComputeHash(Encoding.UTF8.GetBytes(str)); return BitConverter.ToString(hash).Replace("-", string.Empty); } } /// /// Loads the users from the repository /// /// IEnumerable{User}. private async Task> LoadUsers() { var users = UserRepository.RetrieveAllUsers().ToList(); // There always has to be at least one user. if (users.Count == 0) { var name = Environment.UserName; var user = InstantiateNewUser(name); user.DateLastSaved = DateTime.UtcNow; await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); users.Add(user); } return users; } /// /// Refreshes metadata for each user /// /// The cancellation token. /// Task. public Task RefreshUsersMetadata(CancellationToken cancellationToken) { var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(), cancellationToken)).ToList(); return Task.WhenAll(tasks); } /// /// Renames the user. /// /// The user. /// The new name. /// Task. /// user /// public async Task RenameUser(User user, string newName) { if (user == null) { throw new ArgumentNullException("user"); } if (string.IsNullOrEmpty(newName)) { throw new ArgumentNullException("newName"); } if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); } if (user.Name.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } await user.Rename(newName); OnUserUpdated(user); } /// /// Updates the user. /// /// The user. /// user /// public async Task UpdateUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id))) { throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); } user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); OnUserUpdated(user); } public event EventHandler> UserCreated; private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); /// /// Creates the user. /// /// The name. /// User. /// name /// public async Task CreateUser(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { var user = InstantiateNewUser(name); var list = Users.ToList(); list.Add(user); Users = list; user.DateLastSaved = DateTime.UtcNow; await UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs { Argument = user }, _logger); return user; } finally { _userListLock.Release(); } } /// /// Deletes the user. /// /// The user. /// Task. /// user /// public async Task DeleteUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } var allUsers = Users.ToList(); if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) { throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); } if (allUsers.Count == 1) { 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) { 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)); } await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false); var path = user.ConfigurationFilePath; try { File.Delete(path); } catch (IOException ex) { _logger.ErrorException("Error deleting file {0}", ex, path); } // Force this to be lazy loaded again Users = await LoadUsers().ConfigureAwait(false); OnUserDeleted(user); } finally { _userListLock.Release(); } } /// /// Resets the password by clearing it. /// /// Task. public Task ResetPassword(User user) { return ChangePassword(user, string.Empty); } /// /// Changes the password. /// /// The user. /// The new password. /// Task. public Task ChangePassword(User user, string newPassword) { if (user == null) { throw new ArgumentNullException("user"); } user.Password = string.IsNullOrEmpty(newPassword) ? string.Empty : GetSha1String(newPassword); return UpdateUser(user); } /// /// Instantiates the new user. /// /// The name. /// User. private User InstantiateNewUser(string name) { return new User { Name = name, Id = ("MBUser" + name).GetMD5(), DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow }; } public void UpdateConfiguration(User user, UserConfiguration newConfiguration) { var xmlPath = user.ConfigurationFilePath; Directory.CreateDirectory(Path.GetDirectoryName(xmlPath)); _xmlSerializer.SerializeToFile(newConfiguration, xmlPath); EventHelper.FireEventIfNotNull(UserConfigurationUpdated, this, new GenericEventArgs { Argument = user }, _logger); } } }