Skip to content

Commit 846cc3f

Browse files
Refactor file auth to use registry-based handler model
Introduced IUmbrellaFileAuthorizationHandlerRegistry and its default implementation to resolve file authorization handlers by directory or file info. Refactored UmbrellaFileStorageProvider and all derived providers to use the registry for authorization, removing handler lookup logic from options. Added built-in handlers for temp and dynamic image cache files. Updated DI registration for safe, idempotent handler and registry setup. Revised tests for the new model and added registry/authorization flow coverage. Added AddUmbrellaSharePointFileStorageProvider extension. All providers now require the registry in their constructors.
1 parent e22474b commit 846cc3f

24 files changed

Lines changed: 831 additions & 180 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Umbrella.FileSystem.Abstractions;
2+
3+
namespace Umbrella.DynamicImage.Abstractions.Caching;
4+
5+
/// <summary>
6+
/// An authorization handler for files stored in the Dynamic Image cache directory.
7+
/// </summary>
8+
public sealed class UmbrellaDynamicImageCacheFileAuthorizationHandler : IUmbrellaFileAuthorizationHandler
9+
{
10+
private readonly DynamicImageCacheCoreOptions _cacheCoreOptions;
11+
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="UmbrellaDynamicImageCacheFileAuthorizationHandler"/> class.
14+
/// </summary>
15+
/// <param name="cacheCoreOptions">The cache core options.</param>
16+
public UmbrellaDynamicImageCacheFileAuthorizationHandler(DynamicImageCacheCoreOptions cacheCoreOptions)
17+
{
18+
_cacheCoreOptions = cacheCoreOptions;
19+
}
20+
21+
/// <inheritdoc/>
22+
public string DirectoryName => _cacheCoreOptions.DirectoryName;
23+
24+
/// <inheritdoc/>
25+
public Task<bool> AuthorizeAsync(IUmbrellaFileInfo fileInfo, UmbrellaFileOperationType operationType, CancellationToken cancellationToken = default) => Task.FromResult(true);
26+
}

DynamicImage/src/Umbrella.DynamicImage.Abstractions/Caching/UmbrellaDynamicImageCacheFileHandler.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,4 @@ public UmbrellaDynamicImageCacheFileHandler(
3636

3737
/// <inheritdoc/>
3838
public override string DirectoryName => _cacheCoreOptions.DirectoryName;
39-
40-
/// <inheritdoc/>
41-
public override Task<bool> AuthorizeAsync(IUmbrellaFileInfo fileInfo, UmbrellaFileOperationType operationType, CancellationToken cancellationToken = default) => Task.FromResult(true);
42-
}
39+
}

DynamicImage/src/Umbrella.DynamicImage.Abstractions/IServiceCollectionExtensions.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
using CommunityToolkit.Diagnostics;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
34
using Umbrella.DynamicImage.Abstractions;
45
using Umbrella.DynamicImage.Abstractions.Caching;
6+
using Umbrella.FileSystem.Abstractions;
57

68
#pragma warning disable IDE0130
79
namespace Microsoft.Extensions.DependencyInjection;
@@ -45,11 +47,12 @@ public static IServiceCollection AddUmbrellaDynamicImage(this IServiceCollection
4547
{
4648
Guard.IsNotNull(services);
4749

48-
_ = services.AddSingleton<IDynamicImageUtility, DynamicImageUtility>();
49-
_ = services.AddSingleton<IDynamicImageCache, DynamicImageNoCache>();
50-
_ = services.AddSingleton<IUmbrellaDynamicImageCacheFileHandler, UmbrellaDynamicImageCacheFileHandler>();
51-
_ = services.AddSingleton<DynamicImageCacheCoreOptions>();
50+
services.TryAddSingleton<IDynamicImageUtility, DynamicImageUtility>();
51+
services.TryAddSingleton<IDynamicImageCache, DynamicImageNoCache>();
52+
services.TryAddSingleton<IUmbrellaDynamicImageCacheFileHandler>(static x => ActivatorUtilities.CreateInstance<UmbrellaDynamicImageCacheFileHandler>(x));
53+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUmbrellaFileAuthorizationHandler, UmbrellaDynamicImageCacheFileAuthorizationHandler>());
54+
services.TryAddSingleton<DynamicImageCacheCoreOptions>();
5255

5356
return services;
5457
}
55-
}
58+
}

DynamicImage/test/Umbrella.DynamicImage.Test/Caching/DynamicImageCacheTest.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11

22
using CommunityToolkit.Diagnostics;
3-
using Microsoft.Extensions.DependencyInjection;
43
using Umbrella.DynamicImage.Abstractions;
54
using Umbrella.DynamicImage.Abstractions.Caching;
65
using Umbrella.DynamicImage.Caching.AzureStorage;
76
using Umbrella.DynamicImage.Caching.Disk;
7+
using Umbrella.FileSystem.Abstractions;
88
using Umbrella.FileSystem.AzureStorage;
99
using Umbrella.FileSystem.Disk;
1010
using Umbrella.Internal.Mocks;
@@ -183,12 +183,11 @@ private static DynamicImageDiskCache CreateDynamicImageDiskCache()
183183
AllowUnhandledFileAuthorizationChecks = true
184184
};
185185

186-
options.Initialize(new ServiceCollection(), new ServiceCollection().BuildServiceProvider());
187-
188186
var provider = new UmbrellaDiskFileStorageProvider(
189187
CoreUtilitiesMocks.CreateLoggerFactory<UmbrellaDiskFileStorageProvider>(),
190188
CoreUtilitiesMocks.CreateMimeTypeUtility(("png", "image/png"), ("jpg,", "image/jpg")),
191-
CoreUtilitiesMocks.CreateGenericTypeConverter());
189+
CoreUtilitiesMocks.CreateGenericTypeConverter(),
190+
CreateAuthorizationHandlerRegistry());
192191

193192
provider.InitializeOptions(options);
194193

@@ -216,12 +215,11 @@ private static DynamicImageAzureBlobStorageCache CreateDynamicImageAzureBlobStor
216215
AllowUnhandledFileAuthorizationChecks = true
217216
};
218217

219-
options.Initialize(new ServiceCollection(), new ServiceCollection().BuildServiceProvider());
220-
221218
var provider = new UmbrellaAzureBlobStorageFileProvider(
222219
CoreUtilitiesMocks.CreateLoggerFactory<UmbrellaAzureBlobStorageFileProvider>(),
223220
CoreUtilitiesMocks.CreateMimeTypeUtility(("png", "image/png"), ("jpg,", "image/jpg")),
224-
CoreUtilitiesMocks.CreateGenericTypeConverter());
221+
CoreUtilitiesMocks.CreateGenericTypeConverter(),
222+
CreateAuthorizationHandlerRegistry());
225223

226224
provider.InitializeOptions(options);
227225

@@ -235,4 +233,7 @@ private static DynamicImageAzureBlobStorageCache CreateDynamicImageAzureBlobStor
235233
provider,
236234
blobStorageCacheOptions);
237235
}
238-
}
236+
237+
private static UmbrellaFileAuthorizationHandlerRegistry CreateAuthorizationHandlerRegistry()
238+
=> new UmbrellaFileAuthorizationHandlerRegistry([]);
239+
}

FileSystem/src/Umbrella.FileSystem.Abstractions/IServiceCollectionExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
using CommunityToolkit.Diagnostics;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
34
using Umbrella.FileSystem.Abstractions;
45

56
#pragma warning disable IDE0130
@@ -21,9 +22,11 @@ public static IServiceCollection AddUmbrellaFileSystemCore(this IServiceCollecti
2122
{
2223
Guard.IsNotNull(services);
2324

24-
_ = services.AddSingleton<IUmbrellaFileStorageProviderFactory, UmbrellaFileStorageProviderFactory>();
25-
_ = services.AddSingleton<IUmbrellaTempFileHandler, UmbrellaTempFileHandler>();
25+
services.TryAddSingleton<IUmbrellaFileStorageProviderFactory, UmbrellaFileStorageProviderFactory>();
26+
services.TryAddSingleton<IUmbrellaFileAuthorizationHandlerRegistry, UmbrellaFileAuthorizationHandlerRegistry>();
27+
services.TryAddSingleton<IUmbrellaTempFileHandler, UmbrellaTempFileHandler>();
28+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IUmbrellaFileAuthorizationHandler, UmbrellaTempFileAuthorizationHandler>());
2629

2730
return services;
2831
}
29-
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Umbrella.FileSystem.Abstractions;
2+
3+
/// <summary>
4+
/// Resolves <see cref="IUmbrellaFileAuthorizationHandler"/> instances for stored files.
5+
/// </summary>
6+
public interface IUmbrellaFileAuthorizationHandlerRegistry
7+
{
8+
/// <summary>
9+
/// Gets the authorization handler for the specified directory name.
10+
/// </summary>
11+
/// <param name="directoryName">The directory name.</param>
12+
/// <returns>The authorization handler if one is registered; otherwise <see langword="null"/>.</returns>
13+
IUmbrellaFileAuthorizationHandler? GetByDirectoryName(string directoryName);
14+
15+
/// <summary>
16+
/// Gets the authorization handler for the specified file.
17+
/// </summary>
18+
/// <param name="fileInfo">The file information.</param>
19+
/// <returns>The authorization handler if one is registered; otherwise <see langword="null"/>.</returns>
20+
IUmbrellaFileAuthorizationHandler? GetByFileInfo(IUmbrellaFileInfo fileInfo);
21+
}

FileSystem/src/Umbrella.FileSystem.Abstractions/IUmbrellaFileHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Umbrella.FileSystem.Abstractions;
66
/// A handler used to access files stored using an implementation of the <see cref="UmbrellaFileStorageProvider{TFileInfo, TOptions}"/>.
77
/// </summary>
88
/// <typeparam name="TGroupId">The type of the group identifier.</typeparam>
9-
public interface IUmbrellaFileHandler<TGroupId> : IUmbrellaFileAuthorizationHandler
9+
public interface IUmbrellaFileHandler<TGroupId>
1010
where TGroupId : IEquatable<TGroupId>
1111
{
1212
/// <summary>

FileSystem/src/Umbrella.FileSystem.Abstractions/IUmbrellaFileStorageProvider.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public interface IUmbrellaFileStorageProvider
1515
/// <summary>
1616
/// Creates a new file at the specified <paramref name="subpath"/>.
1717
/// </summary>
18+
/// <remarks>
19+
/// Implementations materialize a new file instance first and then apply authorization using
20+
/// <see cref="UmbrellaFileOperationType.Create"/> rather than the standard read path used for existing files.
21+
/// </remarks>
1822
/// <param name="subpath">The subpath.</param>
1923
/// <param name="cancellationToken">The cancellation token.</param>
2024
/// <returns>The created file.</returns>
@@ -23,6 +27,9 @@ public interface IUmbrellaFileStorageProvider
2327
/// <summary>
2428
/// Gets the file at the specified <paramref name="subpath"/>.
2529
/// </summary>
30+
/// <remarks>
31+
/// This resolves an existing file and applies authorization using <see cref="UmbrellaFileOperationType.Read"/>.
32+
/// </remarks>
2633
/// <param name="subpath">The subpath.</param>
2734
/// <param name="cancellationToken">The cancellation token.</param>
2835
/// <returns>The file.</returns>
@@ -142,4 +149,4 @@ public interface IUmbrellaFileStorageProvider
142149
/// <param name="cancellationToken">The cancellation token.</param>
143150
/// <returns>A collection of the files in the directory.</returns>
144151
Task<IReadOnlyCollection<IUmbrellaFileInfo>> EnumerateDirectoryAsync(string subpath, CancellationToken cancellationToken = default);
145-
}
152+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace Umbrella.FileSystem.Abstractions;
4+
5+
/// <summary>
6+
/// Serves as the base class for file authorization handlers.
7+
/// </summary>
8+
public abstract class UmbrellaFileAuthorizationHandler : IUmbrellaFileAuthorizationHandler
9+
{
10+
/// <summary>
11+
/// Gets the logger.
12+
/// </summary>
13+
protected ILogger Logger { get; }
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="UmbrellaFileAuthorizationHandler"/> class.
17+
/// </summary>
18+
/// <param name="logger">The logger.</param>
19+
protected UmbrellaFileAuthorizationHandler(ILogger logger)
20+
{
21+
Logger = logger;
22+
}
23+
24+
/// <inheritdoc />
25+
public abstract string DirectoryName { get; }
26+
27+
/// <inheritdoc />
28+
public abstract Task<bool> AuthorizeAsync(IUmbrellaFileInfo fileInfo, UmbrellaFileOperationType operationType, CancellationToken cancellationToken = default);
29+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using CommunityToolkit.Diagnostics;
2+
3+
namespace Umbrella.FileSystem.Abstractions;
4+
5+
/// <summary>
6+
/// The default <see cref="IUmbrellaFileAuthorizationHandlerRegistry"/> implementation.
7+
/// </summary>
8+
public class UmbrellaFileAuthorizationHandlerRegistry : IUmbrellaFileAuthorizationHandlerRegistry
9+
{
10+
private readonly Dictionary<string, IUmbrellaFileAuthorizationHandler> _handlerMappings;
11+
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="UmbrellaFileAuthorizationHandlerRegistry"/> class.
14+
/// </summary>
15+
/// <param name="authorizationHandlers">The registered authorization handlers.</param>
16+
/// <exception cref="UmbrellaFileSystemException">
17+
/// Multiple <see cref="IUmbrellaFileAuthorizationHandler"/> instances were registered for the same normalized directory name.
18+
/// </exception>
19+
public UmbrellaFileAuthorizationHandlerRegistry(IEnumerable<IUmbrellaFileAuthorizationHandler> authorizationHandlers)
20+
{
21+
Guard.IsNotNull(authorizationHandlers);
22+
23+
var handlerMappings = new Dictionary<string, IUmbrellaFileAuthorizationHandler>(StringComparer.Ordinal);
24+
25+
foreach (IUmbrellaFileAuthorizationHandler authorizationHandler in authorizationHandlers)
26+
{
27+
string normalizedDirectoryName = NormalizeDirectoryName(authorizationHandler.DirectoryName);
28+
29+
if (handlerMappings.ContainsKey(normalizedDirectoryName))
30+
{
31+
throw new UmbrellaFileSystemException(
32+
$"Multiple {nameof(IUmbrellaFileAuthorizationHandler)} instances were registered for the normalized directory name '{normalizedDirectoryName}'. Directory names must be unique.");
33+
}
34+
35+
handlerMappings.Add(normalizedDirectoryName, authorizationHandler);
36+
}
37+
38+
_handlerMappings = handlerMappings;
39+
}
40+
41+
/// <inheritdoc />
42+
public IUmbrellaFileAuthorizationHandler? GetByDirectoryName(string directoryName)
43+
{
44+
Guard.IsNotNullOrWhiteSpace(directoryName);
45+
46+
string normalizedDirectoryName = NormalizeDirectoryName(directoryName);
47+
48+
return _handlerMappings.TryGetValue(normalizedDirectoryName, out IUmbrellaFileAuthorizationHandler? authorizationHandler)
49+
? authorizationHandler
50+
: null;
51+
}
52+
53+
/// <inheritdoc />
54+
public IUmbrellaFileAuthorizationHandler? GetByFileInfo(IUmbrellaFileInfo fileInfo)
55+
{
56+
Guard.IsNotNull(fileInfo);
57+
58+
string? directoryName = GetTopLevelDirectoryName(fileInfo.SubPath);
59+
60+
if (string.IsNullOrWhiteSpace(directoryName))
61+
return null;
62+
63+
return GetByDirectoryName(directoryName!);
64+
}
65+
66+
private static string? GetTopLevelDirectoryName(string subPath)
67+
{
68+
if (string.IsNullOrWhiteSpace(subPath))
69+
return null;
70+
71+
string normalizedSubPath = subPath.Trim().Replace('\\', '/').Trim('/');
72+
73+
if (string.IsNullOrWhiteSpace(normalizedSubPath))
74+
return null;
75+
76+
#if NETSTANDARD2_0
77+
int firstSeparatorIndex = normalizedSubPath.IndexOf('/');
78+
#else
79+
int firstSeparatorIndex = normalizedSubPath.IndexOf('/', StringComparison.Ordinal);
80+
#endif
81+
82+
return firstSeparatorIndex >= 0
83+
? normalizedSubPath[..firstSeparatorIndex]
84+
: normalizedSubPath;
85+
}
86+
87+
private static string NormalizeDirectoryName(string directoryName)
88+
{
89+
string normalizedDirectoryName = directoryName.Trim().Replace('\\', '/').Trim('/');
90+
91+
if (string.IsNullOrWhiteSpace(normalizedDirectoryName))
92+
throw new UmbrellaFileSystemException($"The {nameof(IUmbrellaFileAuthorizationHandler.DirectoryName)} value cannot be empty.");
93+
94+
return normalizedDirectoryName.ToLowerInvariant();
95+
}
96+
}

0 commit comments

Comments
 (0)