diff options
Diffstat (limited to 'Timeline')
-rw-r--r-- | Timeline/Auth/Attribute.cs (renamed from Timeline/Authentication/Attribute.cs) | 2 | ||||
-rw-r--r-- | Timeline/Auth/MyAuthenticationHandler.cs (renamed from Timeline/Authentication/AuthHandler.cs) | 18 | ||||
-rw-r--r-- | Timeline/Auth/PrincipalExtensions.cs (renamed from Timeline/Authentication/PrincipalExtensions.cs) | 2 | ||||
-rw-r--r-- | Timeline/Controllers/Testing/TestingAuthController.cs | 2 | ||||
-rw-r--r-- | Timeline/Controllers/UserAvatarController.cs | 2 | ||||
-rw-r--r-- | Timeline/Controllers/UserController.cs | 2 | ||||
-rw-r--r-- | Timeline/Controllers/UserDetailController.cs | 5 | ||||
-rw-r--r-- | Timeline/Filters/User.cs | 66 | ||||
-rw-r--r-- | Timeline/Formatters/StringInputFormatter.cs | 27 | ||||
-rw-r--r-- | Timeline/Resources/Filters.Designer.cs | 36 | ||||
-rw-r--r-- | Timeline/Resources/Filters.resx | 12 | ||||
-rw-r--r-- | Timeline/Resources/Filters.zh.resx | 3 | ||||
-rw-r--r-- | Timeline/Startup.cs | 18 |
13 files changed, 165 insertions, 30 deletions
diff --git a/Timeline/Authentication/Attribute.cs b/Timeline/Auth/Attribute.cs index 370b37e1..86d0109b 100644 --- a/Timeline/Authentication/Attribute.cs +++ b/Timeline/Auth/Attribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization;
using Timeline.Entities;
-namespace Timeline.Authentication
+namespace Timeline.Auth
{
public class AdminAuthorizeAttribute : AuthorizeAttribute
{
diff --git a/Timeline/Authentication/AuthHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index 2b457eb1..f5dcd697 100644 --- a/Timeline/Authentication/AuthHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -11,15 +11,15 @@ using Timeline.Models; using Timeline.Services;
using static Timeline.Resources.Authentication.AuthHandler;
-namespace Timeline.Authentication
+namespace Timeline.Auth
{
- static class AuthConstants
+ public static class AuthenticationConstants
{
public const string Scheme = "Bearer";
public const string DisplayName = "My Jwt Auth Scheme";
}
- public class AuthOptions : AuthenticationSchemeOptions
+ public class MyAuthenticationOptions : AuthenticationSchemeOptions
{
/// <summary>
/// The query param key to search for token. If null then query params are not searched for token. Default to <c>"token"</c>.
@@ -27,15 +27,15 @@ namespace Timeline.Authentication public string TokenQueryParamKey { get; set; } = "token";
}
- public class AuthHandler : AuthenticationHandler<AuthOptions>
+ public class MyAuthenticationHandler : AuthenticationHandler<MyAuthenticationOptions>
{
- private readonly ILogger<AuthHandler> _logger;
+ private readonly ILogger<MyAuthenticationHandler> _logger;
private readonly IUserService _userService;
- public AuthHandler(IOptionsMonitor<AuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService)
+ public MyAuthenticationHandler(IOptionsMonitor<MyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService)
: base(options, logger, encoder, clock)
{
- _logger = logger.CreateLogger<AuthHandler>();
+ _logger = logger.CreateLogger<MyAuthenticationHandler>();
_userService = userService;
}
@@ -80,14 +80,14 @@ namespace Timeline.Authentication {
var userInfo = await _userService.VerifyToken(token);
- var identity = new ClaimsIdentity(AuthConstants.Scheme);
+ var identity = new ClaimsIdentity(AuthenticationConstants.Scheme);
identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String));
identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String)));
var principal = new ClaimsPrincipal();
principal.AddIdentity(identity);
- return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme));
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme));
}
catch (Exception e) when (!(e is ArgumentException))
{
diff --git a/Timeline/Authentication/PrincipalExtensions.cs b/Timeline/Auth/PrincipalExtensions.cs index 8d77ab62..ad7a887f 100644 --- a/Timeline/Authentication/PrincipalExtensions.cs +++ b/Timeline/Auth/PrincipalExtensions.cs @@ -1,7 +1,7 @@ using System.Security.Principal;
using Timeline.Entities;
-namespace Timeline.Authentication
+namespace Timeline.Auth
{
internal static class PrincipalExtensions
{
diff --git a/Timeline/Controllers/Testing/TestingAuthController.cs b/Timeline/Controllers/Testing/TestingAuthController.cs index 67b5b2ef..4d3b3ec7 100644 --- a/Timeline/Controllers/Testing/TestingAuthController.cs +++ b/Timeline/Controllers/Testing/TestingAuthController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Timeline.Authentication;
+using Timeline.Auth;
namespace Timeline.Controllers.Testing
{
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 7c77897d..7625f962 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -6,7 +6,7 @@ using Microsoft.Net.Http.Headers; using System;
using System.Linq;
using System.Threading.Tasks;
-using Timeline.Authentication;
+using Timeline.Auth;
using Timeline.Filters;
using Timeline.Helpers;
using Timeline.Models.Http;
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 7b441c3a..0d950cd7 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Threading.Tasks;
-using Timeline.Authentication;
+using Timeline.Auth;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Http;
diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs index ef13b462..9de9899e 100644 --- a/Timeline/Controllers/UserDetailController.cs +++ b/Timeline/Controllers/UserDetailController.cs @@ -4,6 +4,7 @@ using Timeline.Filters; using Timeline.Models.Validation;
using Timeline.Services;
using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Authorization;
namespace Timeline.Controllers
{
@@ -25,6 +26,8 @@ namespace Timeline.Controllers }
[HttpPut("users/{username}/nickname")]
+ [Authorize]
+ [SelfOrAdmin]
[CatchUserNotExistException]
public async Task<ActionResult> PutNickname([FromRoute][Username] string username,
[FromBody][StringLength(10, MinimumLength = 1)] string body)
@@ -34,6 +37,8 @@ namespace Timeline.Controllers }
[HttpDelete("users/{username}/nickname")]
+ [Authorize]
+ [SelfOrAdmin]
[CatchUserNotExistException]
public async Task<ActionResult> DeleteNickname([FromRoute][Username] string username)
{
diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs index 22fae938..16c76750 100644 --- a/Timeline/Filters/User.cs +++ b/Timeline/Filters/User.cs @@ -1,7 +1,13 @@ -using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using System;
+using Timeline.Auth;
using Timeline.Models.Http;
+using Timeline.Services;
+using static Timeline.Resources.Filters;
namespace Timeline
{
@@ -13,9 +19,10 @@ namespace Timeline {
public static class User // bbb = 101
{
- public const int NotExist = 11010001;
- }
+ public const int NotExist = 11010101;
+ public const int NotSelfOrAdminForbid = 11010201;
+ }
}
}
}
@@ -23,20 +30,59 @@ namespace Timeline namespace Timeline.Filters
{
+ public class SelfOrAdminAttribute : ActionFilterAttribute
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<SelfOrAdminAttribute>>();
+
+ var user = context.HttpContext.User;
+
+ if (user == null)
+ {
+ logger.LogError(LogSelfOrAdminNoUser);
+ return;
+ }
+
+ if (context.ModelState.TryGetValue("username", out var model))
+ {
+ if (model.RawValue is string username)
+ {
+ if (!user.IsAdministrator() && user.Identity.Name != username)
+ {
+ context.Result = new ObjectResult(
+ new CommonResponse(ErrorCodes.Http.Filter.User.NotSelfOrAdminForbid, MessageSelfOrAdminForbid))
+ { StatusCode = StatusCodes.Status403Forbidden };
+ }
+ }
+ else
+ {
+ logger.LogError(LogSelfOrAdminUsernameNotString);
+ }
+ }
+ else
+ {
+ logger.LogError(LogSelfOrAdminNoUsername);
+ }
+ }
+ }
+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")]
public override void OnException(ExceptionContext context)
{
- var body = new CommonResponse(
- ErrorCodes.Http.Filter.User.NotExist,
- Resources.Filters.MessageUserNotExist);
+ if (context.Exception is UserNotExistException)
+ {
+ var body = new CommonResponse(ErrorCodes.Http.Filter.User.NotExist, MessageUserNotExist);
- if (context.HttpContext.Request.Method == "GET")
- context.Result = new NotFoundObjectResult(body);
- else
- context.Result = new BadRequestObjectResult(body);
+ if (context.HttpContext.Request.Method == "GET")
+ context.Result = new NotFoundObjectResult(body);
+ else
+ context.Result = new BadRequestObjectResult(body);
+ }
}
}
}
diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs new file mode 100644 index 00000000..90847e36 --- /dev/null +++ b/Timeline/Formatters/StringInputFormatter.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+using System.IO;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Timeline.Formatters
+{
+ public class StringInputFormatter : TextInputFormatter
+ {
+ public StringInputFormatter()
+ {
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Text.Plain));
+ SupportedEncodings.Add(Encoding.UTF8);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
+ public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
+ {
+ var request = context.HttpContext.Request;
+ using var reader = new StreamReader(request.Body, effectiveEncoding);
+ var stringContent = await reader.ReadToEndAsync();
+ return await InputFormatterResult.SuccessAsync(stringContent);
+ }
+ }
+}
diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index e3c8be41..3481e4ae 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -61,6 +61,33 @@ namespace Timeline.Resources { }
/// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute..
+ /// </summary>
+ internal static string LogSelfOrAdminNoUser {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminNoUser", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but it does not have a model named username..
+ /// </summary>
+ internal static string LogSelfOrAdminNoUsername {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminNoUsername", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string..
+ /// </summary>
+ internal static string LogSelfOrAdminUsernameNotString {
+ get {
+ return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Header Content-Length is missing or of bad format..
/// </summary>
internal static string MessageHeaderContentLengthMissing {
@@ -88,6 +115,15 @@ namespace Timeline.Resources { }
/// <summary>
+ /// Looks up a localized string similar to You can't access the resource unless you are the owner or administrator..
+ /// </summary>
+ internal static string MessageSelfOrAdminForbid {
+ get {
+ return ResourceManager.GetString("MessageSelfOrAdminForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The user does not exist..
/// </summary>
internal static string MessageUserNotExist {
diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index ba1fcee8..b91d4612 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -117,6 +117,15 @@ <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
+ <data name="LogSelfOrAdminNoUser" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute.</value>
+ </data>
+ <data name="LogSelfOrAdminNoUsername" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, but it does not have a model named username.</value>
+ </data>
+ <data name="LogSelfOrAdminUsernameNotString" xml:space="preserve">
+ <value>You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string.</value>
+ </data>
<data name="MessageHeaderContentLengthMissing" xml:space="preserve">
<value>Header Content-Length is missing or of bad format.</value>
</data>
@@ -126,6 +135,9 @@ <data name="MessageHeaderContentTypeMissing" xml:space="preserve">
<value>Header Content-Type is required.</value>
</data>
+ <data name="MessageSelfOrAdminForbid" xml:space="preserve">
+ <value>You can't access the resource unless you are the owner or administrator.</value>
+ </data>
<data name="MessageUserNotExist" xml:space="preserve">
<value>The user does not exist.</value>
</data>
diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx index 690a3e39..159ac04a 100644 --- a/Timeline/Resources/Filters.zh.resx +++ b/Timeline/Resources/Filters.zh.resx @@ -126,6 +126,9 @@ <data name="MessageHeaderContentTypeMissing" xml:space="preserve">
<value>缺少必需的请求头Content-Type。</value>
</data>
+ <data name="MessageSelfOrAdminForbid" xml:space="preserve">
+ <value>你无权访问该资源除非你是资源的拥有者或者管理员。</value>
+ </data>
<data name="MessageUserNotExist" xml:space="preserve">
<value>用户不存在。</value>
</data>
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index b44add6f..f6abf36d 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,9 +8,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Globalization;
-using Timeline.Authentication;
+using Timeline.Auth;
using Timeline.Configs;
using Timeline.Entities;
+using Timeline.Formatters;
using Timeline.Helpers;
using Timeline.Services;
@@ -31,17 +32,22 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddControllers()
+ services.AddControllers(setup =>
+ {
+ setup.InputFormatters.Add(new StringInputFormatter());
+ })
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory;
})
- .AddNewtonsoftJson();
+ .AddNewtonsoftJson(); // TODO: Remove this.
services.Configure<JwtConfig>(Configuration.GetSection(nameof(JwtConfig)));
var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
- services.AddAuthentication(AuthConstants.Scheme)
- .AddScheme<AuthOptions, AuthHandler>(AuthConstants.Scheme, AuthConstants.DisplayName, o => { });
+ services.AddAuthentication(AuthenticationConstants.Scheme)
+ .AddScheme<MyAuthenticationOptions, MyAuthenticationHandler>(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { });
+ services.AddAuthorization();
+
var corsConfig = Configuration.GetSection("Cors").Get<string[]>();
services.AddCors(setup =>
@@ -62,8 +68,8 @@ namespace Timeline services.AddScoped<IJwtService, JwtService>();
services.AddTransient<IPasswordService, PasswordService>();
services.AddTransient<IClock, Clock>();
-
services.AddUserAvatarService();
+ services.AddScoped<IUserDetailService, UserDetailService>();
var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>();
|