Skip to content

Commit 4932c09

Browse files
Ensure permission pre-warm added
1 parent 22e5e1d commit 4932c09

File tree

1 file changed

+97
-0
lines changed

1 file changed

+97
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using StackExchange.Redis;
3+
using Superpower.Model;
4+
using UserManagement.Sdk.Abstractions;
5+
6+
namespace ApiIntegrationMvc.Middlewares
7+
{
8+
public sealed class EnsurePermissionsCachedMiddleware
9+
{
10+
private readonly RequestDelegate _next;
11+
private readonly IUserManagementClient _users; // loads from API
12+
private readonly IDatabase _db;
13+
private readonly string _envPrefix;
14+
15+
public EnsurePermissionsCachedMiddleware(RequestDelegate next, IUserManagementClient users, IDatabase db, IHostEnvironment env)
16+
{
17+
_next = next; _users = users; _db = db; _envPrefix = env.EnvironmentName; // "Development", "Staging", "Production", ...
18+
}
19+
20+
public async Task Invoke(HttpContext ctx)
21+
{
22+
var uid = ctx.User.FindFirst("sub")?.Value
23+
?? ctx.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
24+
25+
if (string.IsNullOrEmpty(uid))
26+
{
27+
// Authenticated but no id claim: let request proceed
28+
await _next(ctx);
29+
return;
30+
}
31+
32+
if (!(ctx.User?.Identity?.IsAuthenticated ?? false))
33+
{
34+
await _next(ctx);
35+
return;
36+
}
37+
// Skip for static files quickly
38+
var path = ctx.Request.Path;
39+
if (path.StartsWithSegments("/css") || path.StartsWithSegments("/js") ||
40+
path.StartsWithSegments("/images") || path.Equals("/favicon.ico"))
41+
{
42+
await _next(ctx);
43+
return;
44+
}
45+
46+
// Only for endpoints that require authorization
47+
var endpoint = ctx.GetEndpoint();
48+
var requiresAuth = endpoint?.Metadata.GetMetadata<IAuthorizeData>() is not null;
49+
50+
if (!requiresAuth)
51+
{
52+
await _next(ctx);
53+
return;
54+
}
55+
56+
57+
var dataKey = $"{_envPrefix}:auth:permissions:{uid}";
58+
var cached = await _db.StringGetAsync(dataKey);
59+
60+
if (!cached.HasValue)
61+
{
62+
var lockKey = $"{_envPrefix}:lock:{dataKey}";
63+
var token = Guid.NewGuid().ToString("N");
64+
// Try to acquire lock for up to ~3s
65+
if (await _db.StringSetAsync(lockKey, token, expiry: TimeSpan.FromSeconds(3), when: When.NotExists))
66+
{
67+
try
68+
{
69+
// Double-check after winning the lock
70+
cached = await _db.StringGetAsync(dataKey);
71+
if (!cached.HasValue)
72+
{
73+
var perms = await _users.GetPermissions(int.Parse(uid), ctx.RequestAborted);
74+
var payload = System.Text.Json.JsonSerializer.Serialize(perms.Categories);
75+
await _db.StringSetAsync(dataKey, payload, expiry: TimeSpan.FromMinutes(30));
76+
}
77+
}
78+
finally
79+
{
80+
// Release lock only if still owned
81+
var tran = _db.CreateTransaction();
82+
tran.AddCondition(Condition.StringEqual(lockKey, token));
83+
_ = tran.KeyDeleteAsync(lockKey);
84+
await tran.ExecuteAsync();
85+
}
86+
}
87+
else
88+
{
89+
// Another node is rebuilding — small wait then read again
90+
await Task.Delay(50, ctx.RequestAborted);
91+
}
92+
}
93+
94+
await _next(ctx);
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)