From 713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 18 Jun 2020 18:09:58 +0200 Subject: Add xml comments; Add status codes; Use return instead of exception --- Jellyfin.Api/Controllers/UserController.cs | 260 ++++++++++++++++++++++------- 1 file changed, 199 insertions(+), 61 deletions(-) (limited to 'Jellyfin.Api/Controllers/UserController.cs') diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index ff9373c2d..825219c66 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,11 +1,15 @@ -using System; +#nullable enable +#pragma warning disable CA1801 + +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.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; @@ -13,9 +17,11 @@ 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; @@ -65,51 +71,60 @@ namespace Jellyfin.Api.Controllers /// Optional filter by IsHidden=true or false. /// Optional filter by IsDisabled=true or false. /// Optional filter by IsGuest=true or false. - /// + /// Users returned. + /// An containing the users. [HttpGet] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled, [FromQuery] bool? isGuest) { - return Ok(Get(isHidden, isDisabled, isGuest, false, false)); + 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 GetUsers(null, false, null); + return Ok(GetUsers(false, false, false).Value); } - return Ok(Get(false, false, false, true, true)); + 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("{id}")] // TODO: authorize escapeParentalControl + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); - return Ok(result); } @@ -117,16 +132,20 @@ namespace Jellyfin.Api.Controllers /// Deletes a user. /// /// The user id. - /// A indicating success. + /// User deleted. + /// User not found. + /// A indicating success or a if the user was not found. [HttpDelete("{id}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteUser([FromRoute] Guid id) { var user = _userManager.GetUserById(id); if (user == null) { - throw new ResourceNotFoundException("User not found"); + return NotFound("User not found"); } _sessionManager.RevokeUserTokens(user.Id, null); @@ -138,10 +157,16 @@ namespace Jellyfin.Api.Controllers /// 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("{id}/Authenticate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid id, [FromQuery, BindRequired] string pw, @@ -156,25 +181,22 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) { - throw new MethodNotAllowedException(); + return Forbid("Only sha1 password is not allowed."); } // Password should always be null - return await AuthenticateUserByName(user.Username, null, pw).ConfigureAwait(false); + return await AuthenticateUserByName(user.Username, pw, password).ConfigureAwait(false); } /// /// Authenticates a user by name. /// - /// The username. - /// - /// - /// + /// The request. + /// User authenticated. + /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] - public async Task> AuthenticateUserByName( - [FromQuery, BindRequired] string username, - [FromQuery, BindRequired] string pw, - [FromQuery, BindRequired] string password) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -186,10 +208,10 @@ namespace Jellyfin.Api.Controllers AppVersion = auth.Version, DeviceId = auth.DeviceId, DeviceName = auth.Device, - Password = pw, - PasswordSha1 = password, + Password = request.Pw, + PasswordSha1 = request.Password, RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), - Username = username + Username = request.Username }).ConfigureAwait(false); return Ok(result); @@ -204,22 +226,31 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's password. /// - /// - /// - /// - /// + /// The user id. + /// The current password sha1-hash. + /// The current password as plain text. + /// The new password in plain text. /// Whether to reset the password. - /// A indicating success. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/Password")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateUserPassword( [FromRoute] Guid id, - [FromQuery] string currentPassword, - [FromQuery] string currentPw, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string currentPassword, + [FromBody] string currentPw, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the password."); + } var user = _userManager.GetUserById(id); @@ -243,7 +274,7 @@ namespace Jellyfin.Api.Controllers if (success == null) { - throw new ArgumentException("Invalid user or password entered."); + return Forbid("Invalid user or password entered."); } await _userManager.ChangePassword(user, newPw).ConfigureAwait(false); @@ -259,20 +290,29 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user's easy password. /// - /// - /// - /// - /// - /// A indicating success. + /// The user id. + /// The new password sha1-hash. + /// The new password in plain text. + /// Whether to reset the password. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. [HttpPost("{id}/EasyPassword")] [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateUserEasyPassword( [FromRoute] Guid id, - [FromQuery] string newPassword, - [FromQuery] string newPw, - [FromQuery] bool resetPassword) + [FromBody] string newPassword, + [FromBody] string newPw, + [FromBody] bool resetPassword) { - AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, true)) + { + return Forbid("User is not allowed to update the easy password."); + } var user = _userManager.GetUserById(id); @@ -296,36 +336,128 @@ namespace Jellyfin.Api.Controllers /// /// Updates a user. /// - /// A indicating success. + /// 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("{id}")] [Authorize] - public ActionResult UpdateUser() // TODO: missing UserDto + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUser( + [FromRoute] Guid id, + [FromBody] UserDto updateUser) { - throw new NotImplementedException(); + if (updateUser == null) + { + return BadRequest(); + } + + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + { + return Forbid("User update not allowed."); + } + + var user = _userManager.GetUserById(id); + + 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. - /// A indicating success. + /// 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("{id}/Policy")] [Authorize] - public ActionResult UpdateUserPolicy([FromRoute] Guid id) // TODO: missing UserPolicy + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserPolicy( + [FromRoute] Guid id, + [FromBody] UserPolicy newPolicy) { - throw new NotImplementedException(); + if (newPolicy == null) + { + return BadRequest(); + } + + var user = _userManager.GetUserById(id); + + // 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(id, 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("{id}/Configuration")] [Authorize] - public ActionResult UpdateUserConfiguration([FromRoute] Guid id) // TODO: missing UserConfiguration + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult UpdateUserConfiguration( + [FromRoute] Guid id, + [FromBody] UserConfiguration userConfig) { - throw new NotImplementedException(); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, id, false)) + { + return Forbid("User configuration update not allowed"); + } + + _userManager.UpdateConfiguration(id, userConfig); + + return NoContent(); } /// @@ -333,10 +465,12 @@ namespace Jellyfin.Api.Controllers /// /// The username. /// The password. - /// A indicating success. + /// User created. + /// An of the new user. [HttpPost("/Users/New")] [Authorize(Policy = Policies.RequiresElevation)] - public async Task CreateUserByName( + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateUserByName( [FromBody] string name, [FromBody] string password) { @@ -357,8 +491,10 @@ namespace Jellyfin.Api.Controllers /// 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) @@ -373,15 +509,17 @@ namespace Jellyfin.Api.Controllers /// 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 Ok(result); } - private IEnumerable Get(bool? isHidden, bool? isDisabled, bool? isGuest, bool filterByDevice, bool filterByNetwork) + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; -- cgit v1.2.3