up
This commit is contained in:
		@@ -15,7 +15,8 @@ public class StandardClientProvisioningStoreTests
 | 
			
		||||
    public async Task CreateOrUpdateAsync_HashesSecretAndPersistsDocument()
 | 
			
		||||
    {
 | 
			
		||||
        var store = new TrackingClientStore();
 | 
			
		||||
        var provisioning = new StandardClientProvisioningStore("standard", store);
 | 
			
		||||
        var revocations = new TrackingRevocationStore();
 | 
			
		||||
        var provisioning = new StandardClientProvisioningStore("standard", store, revocations, TimeProvider.System);
 | 
			
		||||
 | 
			
		||||
        var registration = new AuthorityClientRegistration(
 | 
			
		||||
            clientId: "bootstrap-client",
 | 
			
		||||
@@ -63,4 +64,21 @@ public class StandardClientProvisioningStoreTests
 | 
			
		||||
            return ValueTask.FromResult(removed);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private sealed class TrackingRevocationStore : IAuthorityRevocationStore
 | 
			
		||||
    {
 | 
			
		||||
        public List<AuthorityRevocationDocument> Upserts { get; } = new();
 | 
			
		||||
 | 
			
		||||
        public ValueTask UpsertAsync(AuthorityRevocationDocument document, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            Upserts.Add(document);
 | 
			
		||||
            return ValueTask.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public ValueTask<bool> RemoveAsync(string category, string revocationId, CancellationToken cancellationToken)
 | 
			
		||||
            => ValueTask.FromResult(true);
 | 
			
		||||
 | 
			
		||||
        public ValueTask<IReadOnlyList<AuthorityRevocationDocument>> GetActiveAsync(DateTimeOffset asOf, CancellationToken cancellationToken)
 | 
			
		||||
            => ValueTask.FromResult<IReadOnlyList<AuthorityRevocationDocument>>(Array.Empty<AuthorityRevocationDocument>());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
using Mongo2Go;
 | 
			
		||||
@@ -58,6 +59,21 @@ public class StandardPluginRegistrarTests
 | 
			
		||||
        services.AddLogging();
 | 
			
		||||
        services.AddSingleton<IMongoDatabase>(database);
 | 
			
		||||
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
 | 
			
		||||
        var registrar = new StandardPluginRegistrar();
 | 
			
		||||
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));
 | 
			
		||||
@@ -83,6 +99,53 @@ public class StandardPluginRegistrarTests
 | 
			
		||||
        Assert.True(verification.User?.RequiresPasswordReset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Register_LogsWarning_WhenPasswordPolicyWeaker()
 | 
			
		||||
    {
 | 
			
		||||
        using var runner = MongoDbRunner.Start(singleNodeReplSet: true);
 | 
			
		||||
        var client = new MongoClient(runner.ConnectionString);
 | 
			
		||||
        var database = client.GetDatabase("registrar-password-policy");
 | 
			
		||||
 | 
			
		||||
        var configuration = new ConfigurationBuilder()
 | 
			
		||||
            .AddInMemoryCollection(new Dictionary<string, string?>
 | 
			
		||||
            {
 | 
			
		||||
                ["passwordPolicy:minimumLength"] = "6",
 | 
			
		||||
                ["passwordPolicy:requireUppercase"] = "false",
 | 
			
		||||
                ["passwordPolicy:requireLowercase"] = "false",
 | 
			
		||||
                ["passwordPolicy:requireDigit"] = "false",
 | 
			
		||||
                ["passwordPolicy:requireSymbol"] = "false"
 | 
			
		||||
            })
 | 
			
		||||
            .Build();
 | 
			
		||||
 | 
			
		||||
        var manifest = new AuthorityPluginManifest(
 | 
			
		||||
            "standard",
 | 
			
		||||
            "standard",
 | 
			
		||||
            true,
 | 
			
		||||
            typeof(StandardPluginRegistrar).Assembly.GetName().Name,
 | 
			
		||||
            typeof(StandardPluginRegistrar).Assembly.Location,
 | 
			
		||||
            new[] { AuthorityPluginCapabilities.Password },
 | 
			
		||||
            new Dictionary<string, string?>(),
 | 
			
		||||
            "standard.yaml");
 | 
			
		||||
 | 
			
		||||
        var pluginContext = new AuthorityPluginContext(manifest, configuration);
 | 
			
		||||
        var services = new ServiceCollection();
 | 
			
		||||
        var loggerProvider = new CapturingLoggerProvider();
 | 
			
		||||
        services.AddLogging(builder => builder.AddProvider(loggerProvider));
 | 
			
		||||
        services.AddSingleton<IMongoDatabase>(database);
 | 
			
		||||
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());
 | 
			
		||||
 | 
			
		||||
        var registrar = new StandardPluginRegistrar();
 | 
			
		||||
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));
 | 
			
		||||
 | 
			
		||||
        using var provider = services.BuildServiceProvider();
 | 
			
		||||
        _ = provider.GetRequiredService<StandardUserCredentialStore>();
 | 
			
		||||
 | 
			
		||||
        Assert.Contains(loggerProvider.Entries, entry =>
 | 
			
		||||
            entry.Level == LogLevel.Warning &&
 | 
			
		||||
            entry.Category.Contains(typeof(StandardPluginRegistrar).FullName!, StringComparison.Ordinal) &&
 | 
			
		||||
            entry.Message.Contains("weaker password policy", StringComparison.OrdinalIgnoreCase));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public void Register_ForcesPasswordCapability_WhenManifestMissing()
 | 
			
		||||
    {
 | 
			
		||||
@@ -106,6 +169,8 @@ public class StandardPluginRegistrarTests
 | 
			
		||||
        services.AddLogging();
 | 
			
		||||
        services.AddSingleton<IMongoDatabase>(database);
 | 
			
		||||
        services.AddSingleton<IAuthorityClientStore>(new InMemoryClientStore());
 | 
			
		||||
        services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
 | 
			
		||||
        services.AddSingleton(TimeProvider.System);
 | 
			
		||||
 | 
			
		||||
        var registrar = new StandardPluginRegistrar();
 | 
			
		||||
        registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));
 | 
			
		||||
@@ -209,6 +274,61 @@ public class StandardPluginRegistrarTests
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal sealed record CapturedLogEntry(string Category, LogLevel Level, string Message);
 | 
			
		||||
 | 
			
		||||
internal sealed class CapturingLoggerProvider : ILoggerProvider
 | 
			
		||||
{
 | 
			
		||||
    public List<CapturedLogEntry> Entries { get; } = new();
 | 
			
		||||
 | 
			
		||||
    public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, Entries);
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private sealed class CapturingLogger : ILogger
 | 
			
		||||
    {
 | 
			
		||||
        private readonly string category;
 | 
			
		||||
        private readonly List<CapturedLogEntry> entries;
 | 
			
		||||
 | 
			
		||||
        public CapturingLogger(string category, List<CapturedLogEntry> entries)
 | 
			
		||||
        {
 | 
			
		||||
            this.category = category;
 | 
			
		||||
            this.entries = entries;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
 | 
			
		||||
 | 
			
		||||
        public bool IsEnabled(LogLevel logLevel) => true;
 | 
			
		||||
 | 
			
		||||
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
 | 
			
		||||
        {
 | 
			
		||||
            entries.Add(new CapturedLogEntry(category, logLevel, formatter(state, exception)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private sealed class NullScope : IDisposable
 | 
			
		||||
        {
 | 
			
		||||
            public static readonly NullScope Instance = new();
 | 
			
		||||
 | 
			
		||||
            public void Dispose()
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal sealed class StubRevocationStore : IAuthorityRevocationStore
 | 
			
		||||
{
 | 
			
		||||
    public ValueTask UpsertAsync(AuthorityRevocationDocument document, CancellationToken cancellationToken)
 | 
			
		||||
        => ValueTask.CompletedTask;
 | 
			
		||||
 | 
			
		||||
    public ValueTask<bool> RemoveAsync(string category, string revocationId, CancellationToken cancellationToken)
 | 
			
		||||
        => ValueTask.FromResult(false);
 | 
			
		||||
 | 
			
		||||
    public ValueTask<IReadOnlyList<AuthorityRevocationDocument>> GetActiveAsync(DateTimeOffset asOf, CancellationToken cancellationToken)
 | 
			
		||||
        => ValueTask.FromResult<IReadOnlyList<AuthorityRevocationDocument>>(Array.Empty<AuthorityRevocationDocument>());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal sealed class InMemoryClientStore : IAuthorityClientStore
 | 
			
		||||
{
 | 
			
		||||
    private readonly Dictionary<string, AuthorityClientDocument> clients = new(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user