using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; 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.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.AspNetCore.Mvc.ModelBinding; 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; /// /// 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. public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, IServerConfigurationManager config) { _userManager = userManager; _sessionManager = sessionManager; _networkManager = networkManager; _deviceManager = deviceManager; _authContext = authContext; _config = config; } /// /// 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(Policy = Policies.DefaultAuthorization)] [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.IgnoreSchedule)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid userId) { var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.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 ActionResult DeleteUser([FromRoute] Guid userId) { var user = _userManager.GetUserById(userId); _sessionManager.RevokeUserTokens(user.Id, null); _userManager.DeleteUser(userId); return NoContent(); } /// /// Authenticates a user. /// /// The user id. /// The password as plain text. /// The password sha1-hash. /// 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)] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, [FromQuery, BindRequired] string? pw, [FromQuery, BindRequired] string? password) { var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) { return Forbid("Only sha1 password is not allowed."); } // Password should always be null AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, Password = null, 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, BindRequired] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); try { var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest { App = auth.Client, AppVersion = auth.Version, DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, PasswordSha1 = request.Password, RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), Username = request.Username }).ConfigureAwait(false); return result; } catch (SecurityException e) { // rethrow adding IP address to message throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {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(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid userId, [FromBody] UpdateUserPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { return Forbid("User is not allowed to update the password."); } var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } if (request.ResetPassword) { await _userManager.ResetPassword(user).ConfigureAwait(false); } else { var success = await _userManager.AuthenticateUser( user.Username, request.CurrentPw, request.CurrentPw, HttpContext.Connection.RemoteIpAddress.ToString(), false).ConfigureAwait(false); if (success == null) { return Forbid("Invalid user or password entered."); } await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); var currentToken = _authContext.GetAuthorizationInfo(Request).Token; _sessionManager.RevokeUserTokens(user.Id, currentToken); } 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(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid userId, [FromBody] UpdateUserEasyPassword request) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { return Forbid("User is not allowed to update the easy password."); } var user = _userManager.GetUserById(userId); if (user == null) { return NotFound("User not found"); } if (request.ResetPassword) { _userManager.ResetEasyPassword(user); } else { _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); } 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(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUser( [FromRoute] Guid userId, [FromBody] UserDto updateUser) { if (updateUser == null) { return BadRequest(); } if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) { return Forbid("User update not allowed."); } var user = _userManager.GetUserById(userId); if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { await _userManager.UpdateUserAsync(user).ConfigureAwait(false); _userManager.UpdateConfiguration(user.Id, updateUser.Configuration); } else { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration); } 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.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult UpdateUserPolicy( [FromRoute] Guid userId, [FromBody] UserPolicy newPolicy) { if (newPolicy == null) { return BadRequest(); } var user = _userManager.GetUserById(userId); // If removing admin access if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))) { if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) { return Forbid("There must be at least one user in the system with administrative access."); } } // If disabling if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) { return Forbid("Administrators cannot be disabled."); } // If disabling if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) { if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) { return Forbid("There must be at least one enabled user in the system."); } var currentToken = _authContext.GetAuthorizationInfo(Request).Token; _sessionManager.RevokeUserTokens(user.Id, currentToken); } _userManager.UpdatePolicy(userId, newPolicy); 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(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult UpdateUserConfiguration( [FromRoute] Guid userId, [FromBody] UserConfiguration userConfig) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) { return Forbid("User configuration update not allowed"); } _userManager.UpdateConfiguration(userId, userConfig); return NoContent(); } /// /// Creates a user. /// /// The create user by name request body. /// User created. /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateUserByName([FromBody] CreateUserByName request) { var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false); // no need to authenticate password for new user if (request.Password != null) { await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); return result; } /// /// Initiates the forgot password process for a local user. /// /// The entered username. /// Password reset process started. /// A containing a . [HttpPost("ForgotPassword")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPassword([FromBody] string? enteredUsername) { var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()); var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); return result; } /// /// Redeems a forgot password pin. /// /// The pin. /// Pin reset process started. /// A containing a . [HttpPost("ForgotPassword/Pin")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPasswordPin([FromBody] string? pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); return result; } 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 = _authContext.GetAuthorizationInfo(Request).DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); } } if (filterByNetwork) { if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString())) { users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } } var result = users .OrderBy(u => u.Username) .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())); return result; } } }