Переглянути джерело

Introducing user context middleware and ApiUser

Lukas Angerer 3 роки тому
батько
коміт
334eaa7659

+ 32 - 0
src/RunnersMeet.Server/ApiUser.cs

@@ -0,0 +1,32 @@
+using System.Security.Claims;
+
+namespace RunnersMeet.Server;
+
+public class ApiUser
+{
+	private const string UnknownUserId = "<unknown>";
+	private static readonly AsyncLocal<ApiUser> AsyncLocal = new AsyncLocal<ApiUser>();
+
+	public static ApiUser Current =>
+		AsyncLocal.Value ?? throw new InvalidOperationException("No user present in request context");
+
+	public static void Create(ClaimsPrincipal principal)
+	{
+		if (AsyncLocal.Value != null)
+		{
+			throw new InvalidOperationException("User can only be set once per request");
+		}
+
+		AsyncLocal.Value = new ApiUser(principal);
+	}
+
+	public string UserId { get; }
+	public IList<string> Claims { get; }
+	public bool IsValidUser => UserId != UnknownUserId;
+
+	private ApiUser(ClaimsPrincipal principal)
+	{
+		UserId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? UnknownUserId;
+		Claims = principal.FindAll("permissions").Select(claim => claim.Value).ToList();
+	}
+}

+ 2 - 0
src/RunnersMeet.Server/AppServer.cs

@@ -5,6 +5,7 @@ using RunnersMeet.Server.ServerAspects.Auth;
 using RunnersMeet.Server.ServerAspects.Cors;
 using RunnersMeet.Server.ServerAspects.Spa;
 using RunnersMeet.Server.ServerAspects.Swagger;
+using RunnersMeet.Server.ServerAspects.UserContext;
 
 namespace RunnersMeet.Server;
 
@@ -14,6 +15,7 @@ public class AppServer
 	{
 		new AuthModule(),
 		new CorsModule(),
+		new UserContextModule(),
 		new SpaRoutingModule(),
 		new ApiControllersModule(),
 		new SwaggerModule(),

+ 4 - 8
src/RunnersMeet.Server/Controllers/TracksController.cs

@@ -1,4 +1,3 @@
-using System.Security.Claims;
 using LiteDB;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
@@ -35,7 +34,7 @@ public class TracksController : ControllerBase
 		{
 			if (owner == "me")
 			{
-				owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
+				owner = ApiUser.Current.UserId;
 			}
 			query.ForOwner(owner);
 		}
@@ -63,8 +62,7 @@ public class TracksController : ControllerBase
 		try
 		{
 			var gpxSummary = _gpxParser.ExtractSummary(_fileStorage.OpenFileRead(fileName));
-			var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
-			var user = _queryFactory.GetUserQuery().Get(userId);
+			var user = _queryFactory.GetUserQuery().Get(ApiUser.Current.UserId);
 			var track = _queryFactory.CreateTrackCommand().Create(user, fileName, gpxSummary);
 			return Ok(track);
 		}
@@ -90,8 +88,7 @@ public class TracksController : ControllerBase
 		{
 			throw new ArgumentException("Object ID in URL does not match track ID");
 		}
-		var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
-		var result = _queryFactory.UpdateTrackCommand().Update(userId, track);
+		var result = _queryFactory.UpdateTrackCommand().Update(ApiUser.Current.UserId, track);
 
 		return Ok(result);
 	}
@@ -99,8 +96,7 @@ public class TracksController : ControllerBase
 	[HttpDelete("{id}")]
 	public ActionResult DeleteTrack(string id)
 	{
-		var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
-		var fileName = _queryFactory.DeleteTrackCommand().Delete(userId, new ObjectId(id));
+		var fileName = _queryFactory.DeleteTrackCommand().Delete(ApiUser.Current.UserId, new ObjectId(id));
 		_fileStorage.DeleteFile(fileName);
 
 		return Ok();

+ 3 - 5
src/RunnersMeet.Server/Controllers/UsersController.cs

@@ -1,4 +1,3 @@
-using System.Security.Claims;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using RunnersMeet.Server.Domain;
@@ -21,18 +20,17 @@ public class UsersController : ControllerBase
 	[HttpGet("validate")]
 	public ActionResult<UserValidationResult> Validate([FromQuery] string? nickname)
 	{
-		var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
-		if (userId == null)
+		if (!ApiUser.Current.IsValidUser)
 		{
 			throw new ApiException("UsersController.Validate call without a User / authentication token");
 		}
 
-		var userProfile = _queryFactory.ValidateUserCommand().Validate(userId, nickname);
+		var userProfile = _queryFactory.ValidateUserCommand().Validate(ApiUser.Current.UserId, nickname);
 
 		return new UserValidationResult
 		{
 			UserProfile = userProfile,
-			Claims = User.FindAll("permissions").Select(claim => claim.Value),
+			Claims = ApiUser.Current.Claims,
 		};
 	}
 }

+ 17 - 0
src/RunnersMeet.Server/ServerAspects/UserContext/UserContextMiddleware.cs

@@ -0,0 +1,17 @@
+namespace RunnersMeet.Server.ServerAspects.UserContext;
+
+public class UserContextMiddleware
+{
+	private readonly RequestDelegate _next;
+
+	public UserContextMiddleware(RequestDelegate next)
+	{
+		_next = next;
+	}
+
+	public async Task InvokeAsync(HttpContext context)
+	{
+		ApiUser.Create(context.User);
+		await _next(context);
+	}
+}

+ 13 - 0
src/RunnersMeet.Server/ServerAspects/UserContext/UserContextModule.cs

@@ -0,0 +1,13 @@
+namespace RunnersMeet.Server.ServerAspects.UserContext;
+
+public class UserContextModule : IAppConfigurationModule
+{
+	public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
+	{
+	}
+
+	public void ConfigureApplication(WebApplication app)
+	{
+		app.UseMiddleware<UserContextMiddleware>();
+	}
+}