using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; /// /// User controller. /// [Route("Users")] public class UserController : BaseJellyfinApiController { private readonly IUserManager _userManager; private readonly ISessionManager _sessionManager; private readonly INetworkManager _networkManager; private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IQuickConnect _quickConnectManager; private readonly IPlaylistManager _playlistManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, IServerConfigurationManager config, ILogger logger, IQuickConnect quickConnectManager, IPlaylistManager playlistManager) { _userManager = userManager; _sessionManager = sessionManager; _networkManager = networkManager; _deviceManager = deviceManager; _authContext = authContext; _config = config; _logger = logger; _quickConnectManager = quickConnectManager; _playlistManager = playlistManager; } /// /// Gets a list of users. /// /// Optional filter by IsHidden=true or false. /// Optional filter by IsDisabled=true or false. /// Users returned. /// An containing the users. [HttpGet] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled) { var users = Get(isHidden, isDisabled, false, false); return Ok(users); } /// /// Gets a list of publicly visible users for display on a login screen. /// /// Public users returned. /// An containing the public users. [HttpGet("Public")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { return Ok(Get(false, false, false, false)); } return Ok(Get(false, false, true, true)); } /// /// Gets a user by Id. /// /// The user id. /// User returned. /// User not found. /// An with information about the user or a if the user was not found. [HttpGet("{userId}")] [Authorize(Policy = Policies.IgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); if (user is null) { return NotFound("User not found"); } var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } /// /// Deletes a user. /// /// The user id. /// User deleted. /// User not found. /// A indicating success or a if the user was not found. [HttpDelete("{userId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteUser([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); if (user is null) { return NotFound(); } await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); await _playlistManager.RemovePlaylists(userId).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } /// /// Authenticates a user. /// /// The user id. /// The password as plain text. /// User authenticated. /// Sha1-hashed password only is not allowed. /// User not found. /// A containing an . [HttpPost("{userId}/Authenticate")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("Authenticate with username instead")] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, [FromQuery, Required] string pw) { var user = _userManager.GetUserById(userId); if (user is null) { return NotFound("User not found"); } AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, Pw = pw }; return await AuthenticateUserByName(request).ConfigureAwait(false); } /// /// Authenticates a user by name. /// /// The request. /// User authenticated. /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); try { var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest { App = auth.Client, AppVersion = auth.Version, DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), Username = request.Username }).ConfigureAwait(false); return result; } catch (SecurityException e) { // rethrow adding IP address to message throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); } } /// /// Authenticates a user with quick connect. /// /// The request. /// User authenticated. /// Missing token. /// A containing an with information about the new session. [HttpPost("AuthenticateWithQuickConnect")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) { try { return _quickConnectManager.GetAuthorizedRequest(request.Secret); } catch (SecurityException e) { // rethrow adding IP address to message throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); } } /// /// Updates a user's password. /// /// The user id. /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/Password")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); } var user = _userManager.GetUserById(userId); if (user is null) { return NotFound("User not found"); } if (request.ResetPassword) { await _userManager.ResetPassword(user).ConfigureAwait(false); } else { if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId)) { var success = await _userManager.AuthenticateUser( user.Username, request.CurrentPw ?? string.Empty, request.CurrentPw ?? string.Empty, HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); if (success is null) { return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); } } await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false); var currentToken = User.GetToken(); await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } return NoContent(); } /// /// Updates a user's easy password. /// /// The user id. /// The request. /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/EasyPassword")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserEasyPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); } var user = _userManager.GetUserById(userId); if (user is null) { return NotFound("User not found"); } if (request.ResetPassword) { await _userManager.ResetEasyPassword(user).ConfigureAwait(false); } else { await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false); } return NoContent(); } /// /// Updates a user. /// /// The user id. /// The updated user model. /// User updated. /// User information was not supplied. /// User update forbidden. /// A indicating success or a or a on failure. [HttpPost("{userId}")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUser( [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { var user = _userManager.GetUserById(userId); if (user is null) { return NotFound(); } if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); } await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false); return NoContent(); } /// /// Updates a user policy. /// /// The user id. /// The new user policy. /// User policy updated. /// User policy was not supplied. /// User policy update forbidden. /// A indicating success or a or a on failure.. [HttpPost("{userId}/Policy")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUserPolicy( [FromRoute, Required] Guid userId, [FromBody, Required] UserPolicy newPolicy) { var user = _userManager.GetUserById(userId); if (user is null) { return NotFound(); } // If removing admin access if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) { if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) { return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access."); } } // If disabling if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) { return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled."); } // If disabling if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) { if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) { return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); } var currentToken = User.GetToken(); await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false); return NoContent(); } /// /// Updates a user configuration. /// /// The user id. /// The new user configuration. /// User configuration updated. /// User configuration update forbidden. /// A indicating success. [HttpPost("{userId}/Configuration")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUserConfiguration( [FromRoute, Required] Guid userId, [FromBody, Required] UserConfiguration userConfig) { if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); } await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false); return NoContent(); } /// /// Creates a user. /// /// The create user by name request body. /// User created. /// An of the new user. [HttpPost("New")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateUserByName([FromBody, Required] CreateUserByName request) { var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false); // no need to authenticate password for new user if (request.Password is not null) { await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } /// /// Initiates the forgot password process for a local user. /// /// The forgot password request containing the entered username. /// Password reset process started. /// A containing a . [HttpPost("ForgotPassword")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { var ip = HttpContext.GetNormalizedRemoteIp(); var isLocal = HttpContext.IsLocal() || _networkManager.IsInLocalNetwork(ip); if (isLocal) { _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); return result; } /// /// Redeems a forgot password pin. /// /// The forgot password pin request containing the entered pin. /// Pin reset process started. /// A containing a . [HttpPost("ForgotPassword/Pin")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest) { var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false); return result; } /// /// Gets the user based on auth token. /// /// User returned. /// Token is not owned by a user. /// A for the authenticated user. [HttpGet("Me")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult GetCurrentUser() { var userId = User.GetUserId(); if (userId.Equals(default)) { return BadRequest(); } var user = _userManager.GetUserById(userId); if (user is null) { return BadRequest(); } return _userManager.GetUserDto(user); } private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; if (isDisabled.HasValue) { users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value); } if (isHidden.HasValue) { users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value); } if (filterByDevice) { var deviceId = User.GetDeviceId(); if (!string.IsNullOrWhiteSpace(deviceId)) { users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); } } if (filterByNetwork) { if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())) { users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } } var result = users .OrderBy(u => u.Username) .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); return result; } }