From 4367f283f196d8b6f195855b4592bf65aa10859e Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 12 Aug 2019 16:14:57 +0800 Subject: Add username validator. --- Timeline/Services/UserService.cs | 73 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 28218612..50aa4187 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; @@ -102,6 +103,74 @@ namespace Timeline.Services public long RequiredVersion { get; private set; } } + /// + /// Thrown when username is of bad format. + /// + [Serializable] + public class UsernameBadFormatException : Exception + { + public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } + public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } + protected UsernameBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// Username of bad format. + /// + public string Username { get; private set; } + } + + public class UsernameValidator + { + public const int MaxLength = 26; + public const string RegexPattern = @"^[a-zA-Z0-9_][a-zA-Z0-9-_]*$"; + + private readonly Regex _regex = new Regex(RegexPattern); + + /// + /// Validate a username. + /// + /// The username. Can't be null. + /// Set as error message if there is error. Or null if no error. + /// True if validation passed. Otherwise false. + /// Thrown when is null. + public bool Validate(string username, out string message) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + if (username.Length == 0) + { + message = "An empty string is not permitted."; + return false; + } + + if (username.Length > 26) + { + message = $"Too long, more than 26 characters is not premitted, found {username.Length}."; + return false; + } + + foreach ((char c, int i) in username.Select((c, i) => (c, i))) + if (char.IsWhiteSpace(c)) + { + message = $"A whitespace is found at {i} . Whitespace is not permited."; + return false; + } + + var match = _regex.Match(username); + if (!match.Success) + { + message = "Regex match failed."; + return false; + } + + message = null; + return true; + } + } + public interface IUserService { /// @@ -144,14 +213,14 @@ namespace Timeline.Services /// /// Create or modify a user with given username. - /// Return if a new user is created. - /// Return if a existing user is modified. + /// Username must be match with [a-zA-z0-9-_]. /// /// Username of user. /// Password of user. /// Whether the user is administrator. /// Return if a new user is created. /// Return if a existing user is modified. + /// Thrown when is of bad format. /// Thrown when or is null. Task PutUser(string username, string password, bool administrator); -- cgit v1.2.3 From afaad69437dc12ffb706667eb63875b116631234 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 12 Aug 2019 16:24:17 +0800 Subject: Add username format check. --- Timeline.Tests/UserUnitTest.cs | 11 +++++++++++ Timeline/Controllers/UserController.cs | 34 ++++++++++++++++++++++------------ Timeline/Services/UserService.cs | 9 +++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs index 1f72000c..2aa89fe3 100644 --- a/Timeline.Tests/UserUnitTest.cs +++ b/Timeline.Tests/UserUnitTest.cs @@ -78,6 +78,17 @@ namespace Timeline.Tests .Which.Administrator.Should().Be(administrator); } + { + // Put Bad Username. + var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest + { + Password = password, + Administrator = false + }); + res.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Put_BadUsername); + } + { // Put Created. var res = await client.PutAsJsonAsync(url, new UserPutRequest diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 6f2fe77f..d38f96e1 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -18,9 +18,11 @@ namespace Timeline.Controllers { public const int Get_NotExist = -1001; - public const int Patch_NotExist = -2001; + public const int Put_BadUsername = -2001; - public const int ChangePassword_BadOldPassword = -3001; + public const int Patch_NotExist = -3001; + + public const int ChangePassword_BadOldPassword = -4001; } private readonly ILogger _logger; @@ -53,17 +55,25 @@ namespace Timeline.Controllers [HttpPut("users/{username}"), AdminAuthorize] public async Task Put([FromBody] UserPutRequest request, [FromRoute] string username) { - var result = await _userService.PutUser(username, request.Password, request.Administrator.Value); - switch (result) + try + { + var result = await _userService.PutUser(username, request.Password, request.Administrator.Value); + switch (result) + { + case PutResult.Created: + _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username))); + return CreatedAtAction("Get", new { username }, CommonPutResponse.Created); + case PutResult.Modified: + _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username))); + return Ok(CommonPutResponse.Modified); + default: + throw new Exception("Unreachable code."); + } + } + catch (UsernameBadFormatException e) { - case PutResult.Created: - _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username))); - return CreatedAtAction("Get", new { username }, CommonPutResponse.Created); - case PutResult.Modified: - _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username))); - return Ok(CommonPutResponse.Modified); - default: - throw new Exception("Unreachable code."); + _logger.LogInformation(e, FormatLogMessage("Attempt to create a user with bad username failed.", Pair("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Put_BadUsername, "Username is of bad format.")); } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 50aa4187..0993d3dc 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -278,6 +278,8 @@ namespace Timeline.Services private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; + private readonly UsernameValidator _usernameValidator; + public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) { _logger = logger; @@ -285,6 +287,8 @@ namespace Timeline.Services _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; + + _usernameValidator = new UsernameValidator(); } private string GenerateCacheKeyByUserId(long id) => $"user:{id}"; @@ -377,6 +381,11 @@ namespace Timeline.Services if (password == null) throw new ArgumentNullException(nameof(password)); + if (!_usernameValidator.Validate(username, out var message)) + { + throw new UsernameBadFormatException(username, message); + } + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) -- cgit v1.2.3