From dd0097af5c4ccbe25a1faca2286d729c93fd4116 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 23:13:15 +0800 Subject: ... --- Timeline/Controllers/ControllerAuthExtensions.cs | 30 +++++++ Timeline/Controllers/UserController.cs | 99 +++++++++++++----------- Timeline/Models/Http/User.cs | 41 ---------- Timeline/Models/Http/UserController.cs | 27 +++++++ Timeline/Models/PutResult.cs | 17 ---- Timeline/Models/User.cs | 20 ----- Timeline/Models/UserRoleConvert.cs | 44 ----------- Timeline/Resources/Messages.Designer.cs | 36 +++++++++ Timeline/Resources/Messages.resx | 12 +++ Timeline/Services/User.cs | 49 ++++++++++++ Timeline/Services/UserRoleConvert.cs | 44 +++++++++++ Timeline/Services/UserService.cs | 1 - 12 files changed, 250 insertions(+), 170 deletions(-) create mode 100644 Timeline/Controllers/ControllerAuthExtensions.cs delete mode 100644 Timeline/Models/Http/User.cs create mode 100644 Timeline/Models/Http/UserController.cs delete mode 100644 Timeline/Models/PutResult.cs delete mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/UserRoleConvert.cs create mode 100644 Timeline/Services/User.cs create mode 100644 Timeline/Services/UserRoleConvert.cs (limited to 'Timeline') diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs new file mode 100644 index 00000000..81fd2428 --- /dev/null +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Timeline.Auth; +using System; + +namespace Timeline.Controllers +{ + public static class ControllerAuthExtensions + { + public static bool IsAdministrator(this ControllerBase controller) + { + return controller.User != null && controller.User.IsAdministrator(); + } + + public static long GetUserId(this ControllerBase controller) + { + if (controller.User == null) + throw new InvalidOperationException("Failed to get user id because User is null."); + + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + } + } +} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 3305952a..4c585198 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,15 +1,16 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; +using System.Linq; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserController; +using static Timeline.Resources.Messages; namespace Timeline.Controllers { @@ -26,19 +27,20 @@ namespace Timeline.Controllers _userService = userService; } - [HttpGet("users"), AdminAuthorize] + [HttpGet("users")] public async Task> List() { - return Ok(await _userService.GetUsers()); + var users = await _userService.GetUsers(); + return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray()); } - [HttpGet("users/{username}"), AdminAuthorize] + [HttpGet("users/{username}")] public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(user); + return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator())); } catch (UserNotExistException e) { @@ -47,33 +49,53 @@ namespace Timeline.Controllers } } - [HttpPut("users/{username}"), AdminAuthorize] - public async Task> Put([FromBody] UserPutRequest request, [FromRoute][Username] string username) + [HttpPatch("users/{username}"), Authorize] + public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { - var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); - switch (result) + static User Convert(UserPatchRequest body) { - case PutResult.Create: - return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); - case PutResult.Modify: - return Ok(CommonPutResponse.Modify()); - default: - throw new Exception(ExceptionUnknownPutResult); + return new User + { + Username = body.Username, + Password = body.Password, + Administrator = body.Administrator, + Nickname = body.Nickname + }; } - } - [HttpPatch("users/{username}"), AdminAuthorize] - public async Task Patch([FromBody] UserPatchRequest request, [FromRoute][Username] string username) - { - try + if (this.IsAdministrator()) { - await _userService.PatchUser(username, request.Password, request.Administrator); - return Ok(); + try + { + await _userService.ModifyUser(username, Convert(body)); + return Ok(); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } } - catch (UserNotExistException e) + else { - _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); + if (User.Identity.Name != username) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf)); + + if (body.Username != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username)); + + if (body.Password != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password)); + + if (body.Administrator != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); + + await _userService.ModifyUser(this.GetUserId(), Convert(body)); + return Ok(); } } @@ -91,27 +113,10 @@ namespace Timeline.Controllers } } - [HttpPost("userop/changeusername"), AdminAuthorize] - public async Task ChangeUsername([FromBody] ChangeUsernameRequest request) + [HttpPost("userop/create"), AdminAuthorize] + public async Task CreateUser([FromBody] User body) { - try - { - await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - return Ok(); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameNotExist, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } - catch (ConfictException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserController.ChangeUsername_Conflict()); - } - // there is no need to catch bad format exception because it is already checked in model validation. + } [HttpPost("userop/changepassword"), Authorize] @@ -119,7 +124,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); + await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); return Ok(); } catch (BadPasswordException e) diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs deleted file mode 100644 index b3812f48..00000000 --- a/Timeline/Models/Http/User.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - [Obsolete("Remove this.")] - public class UserPutRequest - { - [Required] - public string Password { get; set; } = default!; - [Required] - public bool? Administrator { get; set; } - } - - [Obsolete("Remove this.")] - public class UserPatchRequest - { - public string? Password { get; set; } - public bool? Administrator { get; set; } - } - - public class ChangeUsernameRequest - { - [Required] - [Username] - public string OldUsername { get; set; } = default!; - - [Required] - [Username] - public string NewUsername { get; set; } = default!; - } - - public class ChangePasswordRequest - { - [Required] - public string OldPassword { get; set; } = default!; - [Required] - public string NewPassword { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs new file mode 100644 index 00000000..229ca1e5 --- /dev/null +++ b/Timeline/Models/Http/UserController.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + public class UserPatchRequest + { + [Username] + public string? Username { get; set; } + + [MinLength(1)] + public string? Password { get; set; } + + [Nickname] + public string? Nickname { get; set; } + + public bool? Administrator { get; set; } + } + + public class ChangePasswordRequest + { + [Required(AllowEmptyStrings = false)] + public string OldPassword { get; set; } = default!; + [Required(AllowEmptyStrings = false)] + public string NewPassword { get; set; } = default!; + } +} diff --git a/Timeline/Models/PutResult.cs b/Timeline/Models/PutResult.cs deleted file mode 100644 index cecf86e6..00000000 --- a/Timeline/Models/PutResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Timeline.Models -{ - /// - /// Represents the result of a "put" operation. - /// - public enum PutResult - { - /// - /// Indicates the item did not exist and now is created. - /// - Create, - /// - /// Indicates the item exists already and is modified. - /// - Modify - } -} diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs deleted file mode 100644 index 2cead892..00000000 --- a/Timeline/Models/User.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Timeline.Models.Validation; - -namespace Timeline.Models -{ - public class User - { - [Username] - public string? Username { get; set; } - public bool? Administrator { get; set; } - public string? Nickname { get; set; } - public string? AvatarUrl { get; set; } - - - #region secret - public long? Id { get; set; } - public string? Password { get; set; } - public long? Version { get; set; } - #endregion secret - } -} diff --git a/Timeline/Models/UserRoleConvert.cs b/Timeline/Models/UserRoleConvert.cs deleted file mode 100644 index ade9a799..00000000 --- a/Timeline/Models/UserRoleConvert.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; - -namespace Timeline.Models -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 8c13374f..15101661 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -96,6 +96,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to You are not the resource owner.. + /// + internal static string Common_Forbid_NotSelf { + get { + return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture); + } + } + /// /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. /// @@ -266,5 +275,32 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); } } + + /// + /// Looks up a localized string similar to You can't set permission unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Administrator { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .. + /// + internal static string UserController_Patch_Forbid_Password { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set username unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Username { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index c5228ed5..db56ed02 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -129,6 +129,9 @@ You have no permission to do the operation. + + You are not the resource owner. + Header Content-Length is missing or of bad format. @@ -186,4 +189,13 @@ The new username already exists. + + You can't set permission unless you are administrator. + + + You can't set password unless you are administrator. If you want to change password, use /userop/changepassword . + + + You can't set username unless you are administrator. + \ No newline at end of file diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs new file mode 100644 index 00000000..f63a374e --- /dev/null +++ b/Timeline/Services/User.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using Timeline.Controllers; + +namespace Timeline.Services +{ + public class User + { + public string? Username { get; set; } + public string? Nickname { get; set; } + public string? AvatarUrl { get; set; } + + #region adminsecret + public bool? Administrator { get; set; } + #endregion adminsecret + + #region secret + public long? Id { get; set; } + public string? Password { get; set; } + public long? Version { get; set; } + #endregion secret + } + + public static class UserExtensions + { + public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + var result = new User + { + Username = user.Username, + Nickname = user.Nickname, + AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new + { + user.Username + }) + }; + + if (adminstrator) + { + result.Administrator = user.Administrator; + } + + return result; + } + } +} diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs new file mode 100644 index 00000000..4fa4a7b8 --- /dev/null +++ b/Timeline/Services/UserRoleConvert.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; + +namespace Timeline.Services +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 616e70ba..ff2306c5 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Resources.Services.UserService; -- cgit v1.2.3