Skip to content

Commit 8882adf

Browse files
authored
Clear cache after an apikey deleted. (#2199)
1 parent f24a7d6 commit 8882adf

File tree

2 files changed

+91
-10
lines changed

2 files changed

+91
-10
lines changed

src/ContentRepository/Security/ApiKeys/ApiKeyManager.cs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
using System.Threading;
44
using System.Threading.Tasks;
55
using Microsoft.Extensions.Caching.Memory;
6+
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.Logging;
78
using Microsoft.Extensions.Options;
8-
using Newtonsoft.Json.Linq;
9+
using SenseNet.Communication.Messaging;
910
using SenseNet.Configuration;
1011
using SenseNet.ContentRepository.Storage;
1112
using SenseNet.ContentRepository.Storage.Security;
@@ -21,13 +22,13 @@ internal class ApiKeyManager : IApiKeyManager
2122
{
2223
private const string FeatureName = "apikey";
2324
private readonly ILogger _logger;
25+
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
2426
private readonly ApiKeysOptions _apiKeys;
25-
private readonly MemoryCache _apiKeyCache = new(new MemoryCacheOptions { SizeLimit = 1024 });
2627

2728
public ApiKeyManager(ILogger<ApiKeyManager> logger, IOptions<ApiKeysOptions> apiKeys)
2829
{
2930
_logger = logger;
30-
_apiKeys = apiKeys.Value ?? new ApiKeysOptions();
31+
_apiKeys = apiKeys?.Value ?? new ApiKeysOptions();
3132

3233
var now = DateTime.UtcNow;
3334
var apiKey = _apiKeys.HealthCheckerUser;
@@ -53,7 +54,11 @@ public async Task<IUser> GetUserByApiKeyAsync(string apiKey, CancellationToken c
5354
if (string.IsNullOrEmpty(apiKey))
5455
return null;
5556

56-
if (!_apiKeyCache.TryGetValue<AccessToken>(apiKey, out var token))
57+
MemoryCache cache;
58+
lock (_cacheLock)
59+
cache = _apiKeyCache;
60+
61+
if (!cache.TryGetValue<AccessToken>(apiKey, out var token))
5762
{
5863
token = await AccessTokenVault.GetTokenAsync(apiKey, 0, FeatureName, cancel).ConfigureAwait(false);
5964

@@ -63,7 +68,7 @@ public async Task<IUser> GetUserByApiKeyAsync(string apiKey, CancellationToken c
6368

6469
// cache for 2 minutes
6570
if (token != null)
66-
_apiKeyCache.Set(apiKey, token, new MemoryCacheEntryOptions
71+
cache.Set(apiKey, token, new MemoryCacheEntryOptions
6772
{
6873
AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddMinutes(2)),
6974
Size = 1
@@ -126,25 +131,28 @@ public async System.Threading.Tasks.Task DeleteApiKeyAsync(string apiKey, Cancel
126131
AssertPermissions(token.UserId);
127132

128133
await AccessTokenVault.DeleteTokenAsync(apiKey, cancel).ConfigureAwait(false);
134+
ResetCache();
129135
}
130136

131-
public System.Threading.Tasks.Task DeleteApiKeysByUserAsync(int userId, CancellationToken cancel)
137+
public async System.Threading.Tasks.Task DeleteApiKeysByUserAsync(int userId, CancellationToken cancel)
132138
{
133139
if (userId < 1)
134-
return System.Threading.Tasks.Task.CompletedTask;
140+
return;
135141

136142
AssertPermissions(userId);
137143

138-
return AccessTokenVault.DeleteTokensAsync(userId, 0, FeatureName, cancel);
144+
await AccessTokenVault.DeleteTokensAsync(userId, 0, FeatureName, cancel);
145+
ResetCache();
139146
}
140147

141-
public System.Threading.Tasks.Task DeleteApiKeysAsync(CancellationToken cancel)
148+
public async System.Threading.Tasks.Task DeleteApiKeysAsync(CancellationToken cancel)
142149
{
143150
// user id: -1 or 1
144151
if (Math.Abs(User.Current.Id) != 1)
145152
throw new SenseNetSecurityException("Only administrators may delete all api keys.");
146153

147-
return AccessTokenVault.DeleteTokensByFeatureAsync(FeatureName, cancel);
154+
await AccessTokenVault.DeleteTokensByFeatureAsync(FeatureName, cancel);
155+
ResetCache();
148156
}
149157

150158
private void AssertPermissions(int userId)
@@ -164,5 +172,44 @@ private static bool IsUserAllowed(int userId)
164172
return currentUser.Id == userId ||
165173
Providers.Instance.SecurityHandler.HasPermission(userId, PermissionType.Save);
166174
}
175+
176+
/* ==================================================================================== CACHE */
177+
178+
private readonly object _cacheLock = new();
179+
private MemoryCache _apiKeyCache = CreateCache();
180+
181+
private static MemoryCache CreateCache() => new(new MemoryCacheOptions { SizeLimit = 1024 });
182+
183+
private void ResetCache()
184+
{
185+
new ApiKeyManagerCacheResetDistributedAction().ExecuteAsync(CancellationToken.None).GetAwaiter().GetResult();
186+
}
187+
private void ResetCachePrivate()
188+
{
189+
MemoryCache oldCache;
190+
lock (_cacheLock)
191+
{
192+
oldCache = _apiKeyCache;
193+
_apiKeyCache = CreateCache();
194+
}
195+
oldCache.Dispose();
196+
}
197+
198+
[Serializable]
199+
internal sealed class ApiKeyManagerCacheResetDistributedAction : DistributedAction
200+
{
201+
public override string TraceMessage => null;
202+
203+
public override System.Threading.Tasks.Task DoActionAsync(bool onRemote, bool isFromMe, CancellationToken cancellationToken)
204+
{
205+
// Local echo of my action: Return without doing anything
206+
if (onRemote && isFromMe)
207+
return System.Threading.Tasks.Task.CompletedTask;
208+
var instance = Providers.Instance.Services.GetService<IApiKeyManager>() as ApiKeyManager;
209+
instance?.ResetCachePrivate();
210+
return System.Threading.Tasks.Task.CompletedTask;
211+
}
212+
}
213+
167214
}
168215
}

src/Tests/SenseNet.ContentRepository.Tests/ApiKeyManagerTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using SenseNet.ContentRepository.Storage;
1111
using SenseNet.ContentRepository.Storage.Security;
1212
using SenseNet.Security;
13+
using System.Threading.Channels;
1314

1415
namespace SenseNet.ContentRepository.Tests
1516
{
@@ -121,6 +122,39 @@ public STT.Task ApiKey_GetApiKeys_RegularUser()
121122
});
122123
}
123124

125+
[TestMethod, TestCategory("ApiKey")]
126+
public STT.Task ApiKey_DeleteApiKey()
127+
{
128+
var cancel = new CancellationToken();
129+
return ApiKeyManagerTest(async apiKeyManager =>
130+
{
131+
var testUser = await Node.LoadAsync<User>("/Root/IMS/domain2/user2", cancel);
132+
// Create another apikey
133+
await apiKeyManager.CreateApiKeyAsync(testUser.Id, DateTime.UtcNow.AddMonths(2), cancel);
134+
var apiKeys = await apiKeyManager.GetApiKeysByUserAsync(testUser.Id, cancel);
135+
Assert.AreEqual(2, apiKeys.Length);
136+
// Memorize values nd ensure they are cached.
137+
var apiKeyValues = apiKeys.Select(x => x.Value).ToArray();
138+
var user0 = await apiKeyManager.GetUserByApiKeyAsync(apiKeyValues[0], cancel);
139+
var user1 = await apiKeyManager.GetUserByApiKeyAsync(apiKeyValues[1], cancel);
140+
Assert.IsNotNull(user0);
141+
Assert.IsNotNull(user1);
142+
143+
// ACT
144+
await apiKeyManager.DeleteApiKeysByUserAsync(testUser.Id, cancel);
145+
146+
// ASSERT-1 Storage deleted
147+
apiKeys = await apiKeyManager.GetApiKeysByUserAsync(testUser.Id, cancel);
148+
Assert.AreEqual(0, apiKeys.Length);
149+
150+
// ASSERT-2 Cache deleted
151+
user0 = await apiKeyManager.GetUserByApiKeyAsync(apiKeyValues[0], cancel);
152+
user1 = await apiKeyManager.GetUserByApiKeyAsync(apiKeyValues[1], cancel);
153+
Assert.IsNull(user0);
154+
Assert.IsNull(user1);
155+
});
156+
}
157+
124158
protected STT.Task ApiKeyManagerTest(Func<IApiKeyManager, STT.Task> callback)
125159
{
126160
IApiKeyManager apiKeyManager = null;

0 commit comments

Comments
 (0)