diff --git a/.gitignore b/.gitignore index 941a36c2..556d4c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -339,3 +339,6 @@ ASALocalRun/ # BeatPulse healthcheck temp database healthchecksdb src/Properties/launchSettings.json + +# E2E sensitive data folder +src\MultiFactor.Radius.Adapter.EndToEndTests\Assets\SensitiveData \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/AppConfigConfigurationSourceTests.cs b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/AppConfigConfigurationSourceTests.cs index 69cd843c..45ce2455 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/AppConfigConfigurationSourceTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/AppConfigConfigurationSourceTests.cs @@ -13,7 +13,7 @@ public class AppConfigConfigurationSourceTests public void Load_ShouldLoadAndTransformNames() { var path = TestEnvironment.GetAssetPath("root-all-appsettings-items.config"); - var source = new TestableAppConfigConfigurationSource(path); + var source = new TestableAppConfigConfigurationSource(new RadiusConfigurationFile(path)); source.Load(); @@ -67,7 +67,7 @@ public void Get_ShouldBindAndAllNestedElementsNotBeNull() var path = TestEnvironment.GetAssetPath("root-minimal-multi.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); @@ -88,7 +88,7 @@ public void Get_ShouldBindRadiusReplySection() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "radius-reply-join.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); @@ -119,7 +119,7 @@ public void Get_Single_ShouldBindRadiusReplySection() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "radius-reply-single.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); @@ -138,7 +138,7 @@ public void Get_ShouldBindUserNameTransformRulesSection() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "user-name-transform-rules.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); @@ -167,7 +167,7 @@ public void Get_SingleRule_ShouldBindUserNameTransformRulesSection() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "user-name-transform-single-rule.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); @@ -185,7 +185,7 @@ public void Get_BypassSecondFactorWhenApiUnreachableShouldBeTrueByDefault() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "user-name-transform-rules.config"); var config = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(path)) + .Add(new XmlAppConfigurationSource(new RadiusConfigurationFile(path))) .Build(); var bound = config.BindRadiusAdapterConfig(); diff --git a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusAdapterConfigurationFactoryTests.cs b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusAdapterConfigurationFactoryTests.cs index 066a073b..e48df81e 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusAdapterConfigurationFactoryTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusAdapterConfigurationFactoryTests.cs @@ -1,4 +1,5 @@ using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.XmlAppConfiguration; using MultiFactor.Radius.Adapter.Tests.Fixtures; namespace MultiFactor.Radius.Adapter.Tests.AdapterConfig; @@ -9,7 +10,7 @@ public class RadiusAdapterConfigurationFactoryTests public void CreateMinimalRoot_WithNoEnvVar_ShouldCreate() { var path = TestEnvironment.GetAssetPath("root-minimal-single.config"); - var config = RadiusAdapterConfigurationFactory.Create(path); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path)); Assert.Equal("0.0.0.0:1812", config.AppSettings.AdapterServerEndpoint); Assert.Equal("000", config.AppSettings.RadiusSharedSecret); @@ -34,7 +35,7 @@ public void CreateMinimalRoot_OverrideByEnvVar_ShouldCreate() env.SetEnvironmentVariable("rad_appsettings__LoggingLevel", "Info"); var path = TestEnvironment.GetAssetPath("root-minimal-single.config"); - var config = RadiusAdapterConfigurationFactory.Create(path); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path)); Assert.Equal("0.0.0.0:1818", config.AppSettings.AdapterServerEndpoint); Assert.Equal("888", config.AppSettings.RadiusSharedSecret); @@ -51,7 +52,7 @@ public void CreateClient_WithNoEnvVar_ShouldCreate() { var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client-minimal-for-overriding"); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path), "client-minimal-for-overriding"); Assert.Equal("windows", config.AppSettings.RadiusClientNasIdentifier); Assert.Equal("000", config.AppSettings.RadiusSharedSecret); @@ -78,7 +79,7 @@ public void CreateClient_OverrideByEnvVar_ShouldCreate() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client-minimal-for-overriding"); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path), "client-minimal-for-overriding"); Assert.Equal("Linux", config.AppSettings.RadiusClientNasIdentifier); Assert.Equal("888", config.AppSettings.RadiusSharedSecret); @@ -98,7 +99,7 @@ public void CreateClientWithSpacedName_OverrideByEnvVar_ShouldCreate() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client minimal spaced"); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path), "client minimal spaced"); Assert.Equal("Linux", config.AppSettings.RadiusClientNasIdentifier); }); @@ -119,7 +120,7 @@ public void CreateClient_ComplexPathOverrideByEnvVar_ShouldCreate() var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client-minimal-for-overriding"); + var config = RadiusAdapterConfigurationFactory.Create(new RadiusConfigurationFile(path), "client-minimal-for-overriding"); var attribute = Assert.Single(config.RadiusReply.Attributes.Elements); Assert.NotNull(attribute); diff --git a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusConfigurationFileTests.cs b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusConfigurationFileTests.cs index 9ffad1a2..6318d5c4 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusConfigurationFileTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/AdapterConfig/RadiusConfigurationFileTests.cs @@ -26,25 +26,6 @@ public void Create_CorrectPath_ShouldCreateAndStoreValue(string path) Assert.Equal(path, file.Path); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("file")] - [InlineData("file.conf")] - public void Cast_ToRadConfFileFromIncorrectPathString_ShouldThrow(string path) - { - Assert.Throws(() => (RadiusConfigurationFile)path); - } - - [Theory] - [InlineData("file.config")] - [InlineData("dir/file.config")] - public void Cast_ToRadConfFileFromCorrectPathString_ShouldSuccess(string path) - { - var file = (RadiusConfigurationFile)path; - Assert.Equal(path, file.Path); - } - [Fact] public void Cast_ToStringFromNullRadConfFile_ShouldThrow() { diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/access-request.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/access-request.config new file mode 100644 index 00000000..6aa04e25 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/access-request.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-false-when-api-unreachable.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-false-when-api-unreachable.config new file mode 100644 index 00000000..74bfe02e --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-false-when-api-unreachable.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-true-when-api-unreachable.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-true-when-api-unreachable.config new file mode 100644 index 00000000..512f7ee7 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-bypass-true-when-api-unreachable.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-no-bypass-when-api-unreachable.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-no-bypass-when-api-unreachable.config new file mode 100644 index 00000000..ab909cdd --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root-no-bypass-when-api-unreachable.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root.config new file mode 100644 index 00000000..d14f0d34 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/root.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/status-server.config b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/status-server.config new file mode 100644 index 00000000..1a31df5b --- /dev/null +++ b/src/MultiFactor.Radius.Adapter.Tests/Assets/E2E/BaseConfigs/status-server.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs index 241988f9..3d3a1a7d 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs @@ -8,7 +8,7 @@ namespace MultiFactor.Radius.Adapter.Tests.Fixtures.ConfigLoading; internal class TestClientConfigsProvider : IClientConfigurationsProvider { - private Dictionary _dict = new(); + private Dictionary _dict = new(); private readonly TestConfigProviderOptions _options; public TestClientConfigsProvider(IOptions options) @@ -23,11 +23,23 @@ public RadiusAdapterConfiguration[] GetClientConfigurations() { return Array.Empty(); } - - _dict = clientConfigFiles - .Select(x => new RadiusConfigurationFile(x)) - .ToDictionary(k => k, v => RadiusAdapterConfigurationFactory.Create(v, v.Name)); - + var fileSources = clientConfigFiles.Select(x => new RadiusConfigurationFile(x)).ToArray(); + foreach (var file in fileSources) + { + var config = RadiusAdapterConfigurationFactory.Create(file, file.Name, _options.EnvironmentVariablePrefix); + _dict.Add(file, config); + } + + var envVarSources = DefaultClientConfigurationsProvider.GetEnvVarClients() + .Select(x => new RadiusConfigurationEnvironmentVariable(x)) + .ExceptBy(fileSources.Select(x => RadiusConfigurationSource.TransformName(x.Name)), x => x.Name); + + foreach (var envVarClient in envVarSources) + { + var config = RadiusAdapterConfigurationFactory.Create(envVarClient, _options.EnvironmentVariablePrefix); + _dict.Add(envVarClient, config); + } + return _dict.Select(x => x.Value).ToArray(); } @@ -38,7 +50,7 @@ public RadiusConfigurationSource GetSource(RadiusAdapterConfiguration configurat private IEnumerable GetFiles() { - if (_options.ClientConfigFilePaths != null && _options.ClientConfigFilePaths.Length != 0) + if (_options.ClientConfigFilePaths?.Length > 0) { foreach (var f in _options.ClientConfigFilePaths) { diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs index be14b934..f414d77c 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs @@ -4,5 +4,6 @@ internal class TestConfigProviderOptions { public string? RootConfigFilePath { get; set; } public string? ClientConfigsFolderPath { get; set; } - public string[] ClientConfigFilePaths { get; set; } = Array.Empty(); + public string[] ClientConfigFilePaths { get; set; } = []; + public string? EnvironmentVariablePrefix { get; set; } } diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/Radius/EmptyPacket.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/Radius/EmptyPacket.cs index 686d72e9..fb6dec7f 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/Radius/EmptyPacket.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/Radius/EmptyPacket.cs @@ -5,38 +5,34 @@ namespace MultiFactor.Radius.Adapter.Tests.Fixtures.Radius; internal static class RadiusPacketFactory { - public static IRadiusPacket AccessRequest() + public static IRadiusPacket? AccessRequest(SharedSecret packetSecret = null) { var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 0); - var secret = Convert.ToHexString(GenerateSecret()).ToLower(); - var sharedSecret = new SharedSecret(secret); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); return packet; } - public static IRadiusPacket AccessChallenge() + public static IRadiusPacket? AccessChallenge(SharedSecret packetSecret = null) { var header = RadiusPacketHeader.Create(PacketCode.AccessChallenge, 0); - var secret = Convert.ToHexString(GenerateSecret()).ToLower(); - var sharedSecret = new SharedSecret(secret); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); return packet; } - public static IRadiusPacket AccessReject() + public static IRadiusPacket? AccessReject(SharedSecret packetSecret = null) { var header = RadiusPacketHeader.Create(PacketCode.AccessReject, 0); - var secret = Convert.ToHexString(GenerateSecret()).ToLower(); - var sharedSecret = new SharedSecret(secret); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); return packet; } - public static IRadiusPacket StatusServer() + public static IRadiusPacket? StatusServer(SharedSecret packetSecret = null) { var header = RadiusPacketHeader.Create(PacketCode.StatusServer, 0); - var secret = Convert.ToHexString(GenerateSecret()).ToLower(); - var sharedSecret = new SharedSecret(secret); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); return packet; } diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironment.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironment.cs index 2baf51d0..90ee4148 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironment.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironment.cs @@ -3,7 +3,9 @@ internal enum TestAssetLocation { RootDirectory, - ClientsDirectory + ClientsDirectory, + E2EBaseConfigs, + E2ESensitiveData } internal static class TestEnvironment @@ -22,6 +24,8 @@ public static string GetAssetPath(TestAssetLocation location) return location switch { TestAssetLocation.ClientsDirectory => $"{_assetsFolder}{Path.DirectorySeparatorChar}clients", + TestAssetLocation.E2EBaseConfigs => $"{_assetsFolder}{Path.DirectorySeparatorChar}E2E{Path.DirectorySeparatorChar}BaseConfigs", + TestAssetLocation.E2ESensitiveData => $"{_assetsFolder}{Path.DirectorySeparatorChar}E2E{Path.DirectorySeparatorChar}SensitiveData", _ => _assetsFolder, }; } diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironmentVariables.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironmentVariables.cs index a431d70a..7630c310 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironmentVariables.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestEnvironmentVariables.cs @@ -28,6 +28,23 @@ public static void With(Action action) Environment.SetEnvironmentVariable(name, null); } } + + public static async Task With(Func action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + var names = new HashSet(); + + await action(new TestEnvironmentVariables(names)); + + foreach (var name in names) + { + Environment.SetEnvironmentVariable(name, null); + } + } public TestEnvironmentVariables SetEnvironmentVariable(string name, string value) { @@ -41,4 +58,14 @@ public TestEnvironmentVariables SetEnvironmentVariable(string name, string value return this; } + + public TestEnvironmentVariables SetEnvironmentVariables(Dictionary dictionary) + { + foreach (var env in dictionary) + { + SetEnvironmentVariable(env.Key, env.Value); + } + + return this; + } } diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHost.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHost.cs index 219a440e..e35f63c7 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHost.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHost.cs @@ -29,7 +29,7 @@ public TestHost(IHost host) /// If defined, the client config will be specified. Othervise the client config will be getted from the first element of . /// Setup context action. /// - public RadiusContext CreateContext(IRadiusPacket requestPacket, + public RadiusContext CreateContext(IRadiusPacket? requestPacket, IClientConfiguration? clientConfig = null, Action? setupContext = null) { diff --git a/src/MultiFactor.Radius.Adapter.Tests/MultiFactor.Radius.Adapter.Tests.csproj b/src/MultiFactor.Radius.Adapter.Tests/MultiFactor.Radius.Adapter.Tests.csproj index 4609561c..07d76459 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/MultiFactor.Radius.Adapter.Tests.csproj +++ b/src/MultiFactor.Radius.Adapter.Tests/MultiFactor.Radius.Adapter.Tests.csproj @@ -343,4 +343,10 @@ + + + Always + + + diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/IRadiusPacket.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/IRadiusPacket.cs index 77e89146..b20177bb 100644 --- a/src/MultiFactor.Radius.Adapter/Core/Radius/IRadiusPacket.cs +++ b/src/MultiFactor.Radius.Adapter/Core/Radius/IRadiusPacket.cs @@ -62,6 +62,9 @@ public interface IRadiusPacket void AddAttribute(string name, uint value); void AddAttribute(string name, IPAddress value); void AddAttribute(string name, byte[] value); + + void AddAttributes(IDictionary attributes); + IRadiusPacket UpdateAttribute(string name, string value); void CopyTo(IRadiusPacket packet); IRadiusPacket Clone(); diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/RadiusPacket.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/RadiusPacket.cs index 43b711e9..fc6a3acd 100644 --- a/src/MultiFactor.Radius.Adapter/Core/Radius/RadiusPacket.cs +++ b/src/MultiFactor.Radius.Adapter/Core/Radius/RadiusPacket.cs @@ -276,6 +276,14 @@ public void AddAttribute(string name, byte[] value) AddAttributeObject(name, value); } + public void AddAttributes(IDictionary attributes) + { + foreach (var attr in attributes) + { + AddAttributeObject(attr.Key, attr.Value); + } + } + public IRadiusPacket UpdateAttribute(string name, string value) { if (Attributes.ContainsKey(name)) diff --git a/src/MultiFactor.Radius.Adapter/Extensions/RadiusHostApplicationBuilderExtensions.cs b/src/MultiFactor.Radius.Adapter/Extensions/RadiusHostApplicationBuilderExtensions.cs index 383785d4..155ac9ba 100644 --- a/src/MultiFactor.Radius.Adapter/Extensions/RadiusHostApplicationBuilderExtensions.cs +++ b/src/MultiFactor.Radius.Adapter/Extensions/RadiusHostApplicationBuilderExtensions.cs @@ -12,6 +12,11 @@ using MultiFactor.Radius.Adapter.Services.MultiFactorApi; using Serilog; using System; +using MultiFactor.Radius.Adapter.Server.Pipeline.AccessRequestFilter; +using MultiFactor.Radius.Adapter.Server.Pipeline.FirstFactorAuthentication; +using MultiFactor.Radius.Adapter.Server.Pipeline.PreSecondFactorAuthentication; +using MultiFactor.Radius.Adapter.Server.Pipeline.SecondFactorAuthentication; +using MultiFactor.Radius.Adapter.Server.Pipeline.StatusServer; namespace MultiFactor.Radius.Adapter.Extensions; @@ -60,4 +65,15 @@ public static void AddLogging(this RadiusHostApplicationBuilder builder) builder.InternalHostApplicationBuilder.Logging.ClearProviders(); builder.InternalHostApplicationBuilder.Logging.AddSerilog(logger); } + + public static void AddMiddlewares(this RadiusHostApplicationBuilder builder) + { + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + } } \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ConfigurationBuilderExtensions.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ConfigurationBuilderExtensions.cs index 1a26e591..103c96ed 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ConfigurationBuilderExtensions.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ConfigurationBuilderExtensions.cs @@ -10,7 +10,7 @@ namespace MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationL internal static class ConfigurationBuilderExtensions { - public const string BasePrefix = "RAD_"; + public static string BasePrefix; public static IConfigurationBuilder AddRadiusConfigurationFile(this IConfigurationBuilder configurationBuilder, RadiusConfigurationFile file) { @@ -24,12 +24,17 @@ public static IConfigurationBuilder AddRadiusConfigurationFile(this IConfigurati } public static IConfigurationBuilder AddRadiusEnvironmentVariables(this IConfigurationBuilder configurationBuilder, - string configName = null) + string configName = null, + string customPrefix = null) { var preparedConfigName = RadiusConfigurationSource.TransformName(configName); + + BasePrefix = customPrefix ?? "RAD_"; + var prefix = preparedConfigName == string.Empty ? BasePrefix : $"{BasePrefix}{preparedConfigName}_"; + configurationBuilder.AddEnvironmentVariables(prefix); return configurationBuilder; } diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/DefaultClientConfigurationsProvider.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/DefaultClientConfigurationsProvider.cs index 3845e4f1..0bc07e3d 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/DefaultClientConfigurationsProvider.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/DefaultClientConfigurationsProvider.cs @@ -69,7 +69,7 @@ private Dictionary Load() return dict; } - private static IEnumerable GetEnvVarClients() + internal static IEnumerable GetEnvVarClients() { var patterns = RadiusAdapterConfiguration.KnownSectionNames .Select(x => $"^(?i){ConfigurationBuilderExtensions.BasePrefix}(?[a-zA-Z_]+[a-zA-Z0-9_]*)_{x}") diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/RadiusAdapterConfigurationFactory.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/RadiusAdapterConfigurationFactory.cs index 2e2193f5..fe111192 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/RadiusAdapterConfigurationFactory.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/RadiusAdapterConfigurationFactory.cs @@ -20,11 +20,12 @@ internal static class RadiusAdapterConfigurationFactory /// /// Configuration file path. /// Configuration name. + /// Custom env variable prefix. /// Radius Adapter Configuration /// /// /// - public static RadiusAdapterConfiguration Create(RadiusConfigurationFile file, string name = null) + public static RadiusAdapterConfiguration Create(RadiusConfigurationFile file, string name = null, string envPrefix = null) { if (file is null) { @@ -38,7 +39,7 @@ public static RadiusAdapterConfiguration Create(RadiusConfigurationFile file, st var config = new ConfigurationBuilder() .AddRadiusConfigurationFile(file) - .AddRadiusEnvironmentVariables(name) + .AddRadiusEnvironmentVariables(name, customPrefix: envPrefix) .Build(); var bounded = config.BindRadiusAdapterConfig(); @@ -57,7 +58,7 @@ public static RadiusAdapterConfiguration Create(RadiusConfigurationFile file, st /// Radius Adapter Configuration /// /// - public static RadiusAdapterConfiguration Create(RadiusConfigurationEnvironmentVariable environmentVariable) + public static RadiusAdapterConfiguration Create(RadiusConfigurationEnvironmentVariable environmentVariable, string envPrefix = null) { if (environmentVariable is null) { @@ -65,7 +66,7 @@ public static RadiusAdapterConfiguration Create(RadiusConfigurationEnvironmentVa } var config = new ConfigurationBuilder() - .AddRadiusEnvironmentVariables(environmentVariable.Name) + .AddRadiusEnvironmentVariables(environmentVariable.Name, customPrefix: envPrefix) .Build(); var bounded = config.BindRadiusAdapterConfig(); @@ -82,10 +83,10 @@ public static RadiusAdapterConfiguration Create(RadiusConfigurationEnvironmentVa /// /// Radius Adapter Configuration /// - public static RadiusAdapterConfiguration Create() + public static RadiusAdapterConfiguration Create(string envPrefix = null) { var config = new ConfigurationBuilder() - .AddRadiusEnvironmentVariables() + .AddRadiusEnvironmentVariables(customPrefix: envPrefix) .Build(); var bounded = config.BindRadiusAdapterConfig(); diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ServiceConfigurationFactory.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ServiceConfigurationFactory.cs index add10071..27f6d10c 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ServiceConfigurationFactory.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/ConfigurationLoading/ServiceConfigurationFactory.cs @@ -43,8 +43,7 @@ public IServiceConfiguration CreateConfig(RadiusAdapterConfiguration rootConfigu var apiUrlSetting = appSettings.MultifactorApiUrl; var apiProxySetting = appSettings.MultifactorApiProxy; var apiTimeoutSetting = appSettings.MultifactorApiTimeout; - - + if (string.IsNullOrEmpty(apiUrlSetting)) { throw InvalidConfigurationException.For(x => x.AppSettings.MultifactorApiUrl, diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/Models/RadiusReply/RadiusReplyAttributesSection.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/Models/RadiusReply/RadiusReplyAttributesSection.cs index a6c63e18..cb39ce88 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/Models/RadiusReply/RadiusReplyAttributesSection.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/Models/RadiusReply/RadiusReplyAttributesSection.cs @@ -37,4 +37,14 @@ public RadiusReplyAttribute[] Elements return Array.Empty(); } } + + public RadiusReplyAttributesSection() + { + } + + public RadiusReplyAttributesSection(RadiusReplyAttribute singleElement = null, RadiusReplyAttribute[] elements = null) + { + _elements = elements; + _singleElement = singleElement; + } } diff --git a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs index 5893b694..3fa83475 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs @@ -54,22 +54,5 @@ public static implicit operator string(RadiusConfigurationFile path) return path?.Path ?? throw new InvalidCastException("Unable to cast NULL ConfigPath to STRING"); } - public static implicit operator RadiusConfigurationFile(string path) - { - if (path == null) - { - throw new InvalidCastException("Unable cast NULL to ConfigPath"); - } - - try - { - return new RadiusConfigurationFile(path); - } - catch (Exception ex) - { - throw new InvalidCastException("Invalid configuration path", ex); - } - } - public override string ToString() => FileName; } diff --git a/src/MultiFactor.Radius.Adapter/MultiFactor.Radius.Adapter.csproj b/src/MultiFactor.Radius.Adapter/MultiFactor.Radius.Adapter.csproj index ddeefd57..9a78c532 100644 --- a/src/MultiFactor.Radius.Adapter/MultiFactor.Radius.Adapter.csproj +++ b/src/MultiFactor.Radius.Adapter/MultiFactor.Radius.Adapter.csproj @@ -76,6 +76,7 @@ + diff --git a/src/MultiFactor.Radius.Adapter/Program.cs b/src/MultiFactor.Radius.Adapter/Program.cs index e52843b6..79990e5e 100644 --- a/src/MultiFactor.Radius.Adapter/Program.cs +++ b/src/MultiFactor.Radius.Adapter/Program.cs @@ -1,12 +1,6 @@ using Microsoft.Extensions.Hosting; using MultiFactor.Radius.Adapter.Core.Framework; using MultiFactor.Radius.Adapter.Extensions; -using MultiFactor.Radius.Adapter.Server.Pipeline.AccessChallenge; -using MultiFactor.Radius.Adapter.Server.Pipeline.AccessRequestFilter; -using MultiFactor.Radius.Adapter.Server.Pipeline.FirstFactorAuthentication; -using MultiFactor.Radius.Adapter.Server.Pipeline.PreSecondFactorAuthentication; -using MultiFactor.Radius.Adapter.Server.Pipeline.SecondFactorAuthentication; -using MultiFactor.Radius.Adapter.Server.Pipeline.StatusServer; using System; using System.Text; using System.Threading.Tasks; @@ -20,13 +14,8 @@ builder.AddLogging(); builder.ConfigureApplication(); - builder.UseMiddleware(); - builder.UseMiddleware(); - builder.UseMiddleware(); - builder.UseMiddleware(); - builder.UseMiddleware(); - builder.UseMiddleware(); - builder.UseMiddleware(); + builder.AddMiddlewares(); + host = builder.Build(); host.Run(); } diff --git a/src/MultiFactor.Radius.Adapter/Properties/launchSettings.json b/src/MultiFactor.Radius.Adapter/Properties/launchSettings.json index 90dc75e7..960eb168 100644 --- a/src/MultiFactor.Radius.Adapter/Properties/launchSettings.json +++ b/src/MultiFactor.Radius.Adapter/Properties/launchSettings.json @@ -2,7 +2,6 @@ "profiles": { "MultiFactor.Radius.Adapter": { "commandName": "Project", - "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -12,8 +11,7 @@ "commandName": "Docker", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "localhost", - "LD_DEBUG": "libs" + "ASPNETCORE_ENVIRONMENT": "localhost" }, "publishAllPorts": true, "useSSL": false diff --git a/src/MultiFactor.Radius.Adapter/Server/Pipeline/FirstFactorAuthentication/Processing/RadiusFirstFactorAuthenticationProcessor.cs b/src/MultiFactor.Radius.Adapter/Server/Pipeline/FirstFactorAuthentication/Processing/RadiusFirstFactorAuthenticationProcessor.cs index a5152009..7a0085a3 100644 --- a/src/MultiFactor.Radius.Adapter/Server/Pipeline/FirstFactorAuthentication/Processing/RadiusFirstFactorAuthenticationProcessor.cs +++ b/src/MultiFactor.Radius.Adapter/Server/Pipeline/FirstFactorAuthentication/Processing/RadiusFirstFactorAuthenticationProcessor.cs @@ -100,7 +100,6 @@ private async Task ProcessRadiusAuthAsync(RadiusContext context) else { _logger.LogWarning("Remote Radius Server did not respond on message with id={id}", requestPacket.Header.Identifier); - context.Flags.SkipResponse(); return PacketCode.AccessReject; } } diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Constants/RadiusAdapterConstants.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Constants/RadiusAdapterConstants.cs new file mode 100644 index 00000000..003820a4 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Constants/RadiusAdapterConstants.cs @@ -0,0 +1,18 @@ +namespace Multifactor.Radius.Adapter.EndToEndTests.Constants; + +internal static class RadiusAdapterConstants +{ + public const string LocalHost = "127.0.0.1"; + public const int DefaultRadiusAdapterPort = 1812; + public const string DefaultSharedSecret = "000"; + public const string DefaultNasIdentifier = "e2e"; + + public const string BindUserName = "E2EBindUser"; + public const string BindUserPassword = "Qwerty123!"; + + public const string AdminUserName = "E2EAdminUser"; + public const string AdminUserPassword = "Qwerty123!"; + + public const string ChangePasswordUserName = "E2EPasswordUser"; + public const string ChangePasswordUserPassword = "Qwerty123!"; +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Dockerfile b/src/Multifactor.Radius.Adapter.EndToEndTests/Dockerfile new file mode 100644 index 00000000..34a48536 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +RUN apt-get update && apt-get install -y libldap-2.5-0 +RUN ln -s libldap-2.5.so.0 /usr/lib/x86_64-linux-gnu/libldap.so.2 +WORKDIR /app +EXPOSE 80 +EXPOSE 443 \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/E2EClientConfigurationsProvider.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/E2EClientConfigurationsProvider.cs new file mode 100644 index 00000000..d26d05d3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/E2EClientConfigurationsProvider.cs @@ -0,0 +1,26 @@ +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.XmlAppConfiguration; + +namespace Multifactor.Radius.Adapter.EndToEndTests; + +public class E2EClientConfigurationsProvider : IClientConfigurationsProvider +{ + private readonly Dictionary _clientConfigurations; + + public E2EClientConfigurationsProvider(Dictionary? clientConfigurations) + { + _clientConfigurations = clientConfigurations ?? new Dictionary(); + } + + public RadiusConfigurationSource GetSource(RadiusAdapterConfiguration configuration) + { + return new RadiusConfigurationModel(_clientConfigurations.FirstOrDefault(x => x.Value == configuration).Key); + } + + public RadiusAdapterConfiguration[] GetClientConfigurations() + { + return _clientConfigurations.Select(x => x.Value).ToArray(); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestBase.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestBase.cs new file mode 100644 index 00000000..d1bb9219 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestBase.cs @@ -0,0 +1,240 @@ +using System.Text; +using LdapForNet; +using LdapForNet.Native; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.ConfigLoading; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Udp; +using MultiFactor.Radius.Adapter.Extensions; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ClientLevel; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.RootLevel; +using MultiFactor.Radius.Adapter.Services.Ldap; +using MultiFactor.Radius.Adapter.Services.Ldap.Connection; +using MultiFactor.Radius.Adapter.Services.Ldap.Profile; + +namespace Multifactor.Radius.Adapter.EndToEndTests; + +public abstract class E2ETestBase(RadiusFixtures radiusFixtures) : IDisposable +{ + private IHost? _host; + private ProfileLoader? _profileLoader; + private ClientConfigurationFactory _clientConfigurationFactory; + + private readonly RadiusHostApplicationBuilder _radiusHostApplicationBuilder = RadiusHost.CreateApplicationBuilder([ + "--environment", "Test" + ]); + private readonly RadiusPacketParser _packetParser = radiusFixtures.Parser; + private readonly SharedSecret? _secret = radiusFixtures.SharedSecret; + private readonly UdpSocket _udpSocket = radiusFixtures.UdpSocket; + + private protected async Task StartHostAsync( + string rootConfigName, + string[]? clientConfigFileNames = null, + string? envPrefix = null, + Action? configure = null) + { + _radiusHostApplicationBuilder.AddLogging(); + + _radiusHostApplicationBuilder.Services.AddOptions(); + _radiusHostApplicationBuilder.Services.ReplaceService(prov => + { + var opt = prov.GetRequiredService>().Value; + var rootConfig = TestRootConfigProvider.GetRootConfiguration(opt); + var factory = prov.GetRequiredService(); + + var config = factory.CreateConfig(rootConfig); + config.Validate(); + + return config; + }); + + _radiusHostApplicationBuilder.Services + .ReplaceService(); + + _radiusHostApplicationBuilder.AddMiddlewares(); + + _radiusHostApplicationBuilder.ConfigureApplication(); + + ReplaceRadiusConfigs(rootConfigName, clientConfigFileNames, envPrefix: envPrefix); + + configure?.Invoke(_radiusHostApplicationBuilder); + + _host = _radiusHostApplicationBuilder.Build(); + + _profileLoader = _host.Services.GetService(); + _clientConfigurationFactory = _host.Services.GetService(); + + await _host.StartAsync(); + } + + private protected async Task StartHostAsync( + RadiusAdapterConfiguration rootConfig, + Dictionary? clientConfigs = null, + Action? configure = null) + { + _radiusHostApplicationBuilder.AddLogging(); + + _radiusHostApplicationBuilder.Services.ReplaceService(prov => + { + var factory = prov.GetRequiredService(); + + var config = factory.CreateConfig(rootConfig); + config.Validate(); + + return config; + }); + + var clientConfigsProvider = new E2EClientConfigurationsProvider(clientConfigs); + + _radiusHostApplicationBuilder.Services.ReplaceService(clientConfigsProvider); + + _radiusHostApplicationBuilder.AddMiddlewares(); + + _radiusHostApplicationBuilder.ConfigureApplication(); + + configure?.Invoke(_radiusHostApplicationBuilder); + + _host = _radiusHostApplicationBuilder.Build(); + + _profileLoader = _host.Services.GetService(); + _clientConfigurationFactory = _host.Services.GetService(); + + await _host.StartAsync(); + } + + protected IRadiusPacket SendPacketAsync(IRadiusPacket? radiusPacket) + { + if (radiusPacket is null) + { + throw new ArgumentNullException(nameof(radiusPacket)); + } + + var packetBytes = _packetParser.GetBytes(radiusPacket); + _udpSocket.Send(packetBytes); + + var data = _udpSocket.Receive(); + var parsed = _packetParser.Parse(data.GetBytes(), _secret, radiusPacket.Authenticator.Value); + + return parsed; + } + + protected IRadiusPacket? CreateRadiusPacket(PacketCode packetCode, SharedSecret? secret = null, byte identifier = 0) + { + IRadiusPacket? packet; + switch (packetCode) + { + case PacketCode.AccessRequest: + packet = RadiusPacketFactory.AccessRequest(secret ?? _secret, identifier); + break; + case PacketCode.StatusServer: + packet = RadiusPacketFactory.StatusServer(secret ?? _secret, identifier); + break; + case PacketCode.AccessChallenge: + packet = RadiusPacketFactory.AccessChallenge(secret ?? _secret, identifier); + break; + case PacketCode.AccessReject: + packet = RadiusPacketFactory.AccessReject(secret ?? _secret, identifier); + break; + default: + throw new NotImplementedException(); + } + + return packet; + } + + protected async Task SetAttributeForUserInCatalogAsync( + string userName, + RadiusAdapterConfiguration config, + string attributeName, + object attributeValue) + { + var clientConfiguration = CreateClientConfiguration(config); + + var user = LdapIdentity.ParseUser(userName); + using var connection = LdapConnectionAdapter.CreateAsTechnicalAccAsync( + clientConfiguration.ActiveDirectoryDomain, + clientConfiguration, + NullLogger.Instance); + + var formatter = new BindIdentityFormatter(clientConfiguration); + var serviceUser = LdapIdentity.ParseUser(clientConfiguration.ServiceAccountUser); + await connection.BindAsync(formatter.FormatIdentity(serviceUser, clientConfiguration.ActiveDirectoryDomain), clientConfiguration.ServiceAccountPassword); + + var profile = await _profileLoader.LoadAsync(clientConfiguration, connection, user); + var isFreeIpa = clientConfiguration.IsFreeIpa && clientConfiguration.FirstFactorAuthenticationSource != AuthenticationSource.ActiveDirectory; + var request = BuildModifyRequest(profile.DistinguishedName, attributeName, attributeValue, isFreeIpa); + var response = await connection.SendRequestAsync(request); + + if (response.ResultCode != Native.ResultCode.Success) + { + throw new Exception($"Failed to set attribute: {response.ResultCode}"); + } + } + + protected IClientConfiguration CreateClientConfiguration(RadiusAdapterConfiguration configuration) + { + var serviceConfig = _host.Services.GetService(); + return _clientConfigurationFactory.CreateConfig("e2e", configuration, serviceConfig); + } + + private ModifyRequest BuildModifyRequest( + string dn, + string attributeName, + object attributeValue, + bool isFreeIpa) + { + var attrName = attributeName; + + var attribute = new DirectoryModificationAttribute + { + Name = attrName, + LdapModOperation = Native.LdapModOperation.LDAP_MOD_REPLACE + }; + + var bytes = Encoding.UTF8.GetBytes(attributeValue.ToString()); + if (isFreeIpa) + attribute.Add(bytes); + else + attribute.Add(bytes); + + + return new ModifyRequest(dn, attribute); + } + + private void ReplaceRadiusConfigs( + string rootConfigName, + string[]? clientConfigFileNames = null, + string? envPrefix = null) + { + if (string.IsNullOrEmpty(rootConfigName)) + throw new ArgumentException("Empty config path"); + + var clientConfigs = clientConfigFileNames? + .Select(fileName => TestEnvironment.GetAssetPath(TestAssetLocation.E2EBaseConfigs, fileName)) + .ToArray() ?? []; + + var rootConfig = TestEnvironment.GetAssetPath(TestAssetLocation.E2EBaseConfigs, rootConfigName); + + _radiusHostApplicationBuilder.Services.Configure(x => + { + x.RootConfigFilePath = rootConfig; + x.ClientConfigFilePaths = clientConfigs; + x.EnvironmentVariablePrefix = envPrefix; + }); + } + + public void Dispose() + { + _host?.StopAsync(); + _host?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestsUtils.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestsUtils.cs new file mode 100644 index 00000000..92feffa3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/E2ETestsUtils.cs @@ -0,0 +1,84 @@ +using System.Net; +using Microsoft.Extensions.Logging.Abstractions; +using MultiFactor.Radius.Adapter.Core; +using MultiFactor.Radius.Adapter.Core.Radius; +using MultiFactor.Radius.Adapter.Core.Radius.Attributes; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using Multifactor.Radius.Adapter.EndToEndTests.Udp; + +namespace Multifactor.Radius.Adapter.EndToEndTests; + +internal static class E2ETestsUtils +{ + internal static RadiusPacketParser GetRadiusPacketParser() + { + var appVar = new ApplicationVariables + { + AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) ?? throw new Exception() + }; + var dict = new RadiusDictionary(appVar); + dict.Read(); + + return new RadiusPacketParser(NullLogger.Instance, dict); + } + + internal static UdpSocket GetUdpSocket(string ip, int port) + { + return new UdpSocket(IPAddress.Parse(ip), port); + } + + internal static Dictionary GetEnvironmentVariables(string fileName) + { + var envs = new Dictionary(); + var sensitiveDataPath = TestEnvironment.GetAssetPath(TestAssetLocation.E2ESensitiveData, fileName); + + var lines = File.ReadLines(sensitiveDataPath); + foreach (var line in lines) + { + var parts = line.Split('='); + envs.Add(parts[0].Trim(), parts[1].Trim()); + } + + return envs; + } + + internal static ConfigSensitiveData[] GetConfigSensitiveData(string fileName, string separator = "_") + { + var sensitiveDataPath = TestEnvironment.GetAssetPath(TestAssetLocation.E2ESensitiveData, fileName); + + var lines = File.ReadLines(sensitiveDataPath); + var sensitiveData = new List(); + + foreach (var line in lines) + { + var parts = line.Split(separator); + var data = sensitiveData.FirstOrDefault(x => x.ConfigName == parts[0].Trim()); + if (data != null) + { + data.AddConfigValue(parts[1].Trim(), parts[2].Trim()); + } + else + { + var newElement = new ConfigSensitiveData(parts[0].Trim()); + newElement.AddConfigValue(parts[1].Trim(), parts[2].Trim()); + sensitiveData.Add(newElement); + } + } + + return sensitiveData.ToArray(); + } + + internal static string GetEnvPrefix(string envKey) + { + if (string.IsNullOrWhiteSpace(envKey)) + throw new ArgumentNullException(nameof(envKey)); + var parts = envKey.Split('_'); + if (parts?.Length > 0) + { + return parts[0] + "_"; + } + + throw new ArgumentException($"Invalid env key: {envKey}"); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs new file mode 100644 index 00000000..b9504647 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Options; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.XmlAppConfiguration; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.ConfigLoading; + +internal class TestClientConfigsProvider(IOptions options) : IClientConfigurationsProvider +{ + private readonly Dictionary _dict = new(); + private readonly TestConfigProviderOptions _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + + public RadiusAdapterConfiguration[] GetClientConfigurations() + { + var clientConfigFiles = GetFiles().ToArray(); + if (clientConfigFiles.Length == 0) + { + return []; + } + var fileSources = clientConfigFiles.Select(x => new RadiusConfigurationFile(x)).ToArray(); + foreach (var file in fileSources) + { + var config = RadiusAdapterConfigurationFactory.Create(file, file.Name, _options.EnvironmentVariablePrefix); + _dict.Add(file, config); + } + + var envVarSources = DefaultClientConfigurationsProvider.GetEnvVarClients() + .Select(x => new RadiusConfigurationEnvironmentVariable(x)) + .ExceptBy(fileSources.Select(x => RadiusConfigurationSource.TransformName(x.Name)), x => x.Name); + + foreach (var envVarClient in envVarSources) + { + var config = RadiusAdapterConfigurationFactory.Create(envVarClient, _options.EnvironmentVariablePrefix); + _dict.Add(envVarClient, config); + } + + return _dict.Select(x => x.Value).ToArray(); + } + + public RadiusConfigurationSource GetSource(RadiusAdapterConfiguration configuration) + { + return _dict.FirstOrDefault(x => x.Value == configuration).Key; + } + + private IEnumerable GetFiles() + { + if (_options.ClientConfigFilePaths.Length > 0) + { + foreach (var f in _options.ClientConfigFilePaths) + { + if (File.Exists(f)) + { + yield return f; + } + } + + yield break; + } + + if (string.IsNullOrWhiteSpace(_options.ClientConfigsFolderPath)) + { + yield break; + } + + if (!Directory.Exists(_options.ClientConfigsFolderPath)) + { + yield break; + } + + foreach (var f in Directory.GetFiles(_options.ClientConfigsFolderPath, "*.config")) + { + yield return f; + } + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs new file mode 100644 index 00000000..01032c8c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.ConfigLoading; + +internal class TestConfigProviderOptions +{ + public string? RootConfigFilePath { get; set; } + public string? ClientConfigsFolderPath { get; set; } + public string[] ClientConfigFilePaths { get; set; } = []; + public string? EnvironmentVariablePrefix { get; set; } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs new file mode 100644 index 00000000..784a70c5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.XmlAppConfiguration; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.ConfigLoading; + +internal static class TestRootConfigProvider +{ + public static RadiusAdapterConfiguration GetRootConfiguration(TestConfigProviderOptions options) + { + RadiusConfigurationFile rdsRootConfig; + + if (!string.IsNullOrWhiteSpace(options.RootConfigFilePath)) + { + rdsRootConfig = new RadiusConfigurationFile(options.RootConfigFilePath); + } + else + { + var asm = Assembly.GetAssembly(typeof(RdsEntryPoint)); + if (asm is null) + { + throw new Exception("Main assembly not found"); + } + + var path = $"{asm.Location}.config"; + rdsRootConfig = new RadiusConfigurationFile(path); + } + + var config = RadiusAdapterConfigurationFactory.Create(rdsRootConfig); + return config; + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/EmptyStringsListInput.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/EmptyStringsListInput.cs new file mode 100644 index 00000000..18367374 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/EmptyStringsListInput.cs @@ -0,0 +1,19 @@ +using System.Collections; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures; + +internal class EmptyStringsListInput: IEnumerable +{ + public IEnumerator GetEnumerator() + { + yield return new object[] { string.Empty }; + yield return new object[] { " " }; + yield return new object[] { null }; + yield return new object[] { Environment.NewLine }; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/AttributesBuilder.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/AttributesBuilder.cs new file mode 100644 index 00000000..2194bd11 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/AttributesBuilder.cs @@ -0,0 +1,27 @@ +using LdapForNet; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.LdapResponse +{ + internal class AttributesBuilder(SearchResultAttributeCollection attributes) + { + private readonly SearchResultAttributeCollection _attributes = attributes ?? throw new ArgumentNullException(nameof(attributes)); + + public AttributesBuilder Add(string attribute, params string[] values) + { + if (string.IsNullOrWhiteSpace(attribute)) + { + throw new ArgumentException($"'{nameof(attribute)}' cannot be null or whitespace.", nameof(attribute)); + } + + if (values is null) + { + throw new ArgumentNullException(nameof(values)); + } + + var attr = new DirectoryAttribute { Name = attribute }; + attr.AddValues(values); + _attributes.Add(attr); + return this; + } + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/LdapEntryFactory.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/LdapEntryFactory.cs new file mode 100644 index 00000000..8c4fd431 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/LdapResponse/LdapEntryFactory.cs @@ -0,0 +1,26 @@ +using LdapForNet; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.LdapResponse +{ + internal static class LdapEntryFactory + { + public static LdapEntry Create(string dn, Action? setAttributes = null) + { + if (string.IsNullOrWhiteSpace(dn)) + { + throw new ArgumentException($"'{nameof(dn)}' cannot be null or whitespace.", nameof(dn)); + } + + var entry = new LdapEntry + { + Dn = dn, + DirectoryAttributes = new SearchResultAttributeCollection() + }; + + var builder = new AttributesBuilder(entry.DirectoryAttributes); + setAttributes?.Invoke(builder); + + return entry; + } + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs new file mode 100644 index 00000000..3a79ac21 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs @@ -0,0 +1,34 @@ +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; + +public class ConfigSensitiveData +{ + public string ConfigName { get; } + public Dictionary Data { get; } + + public ConfigSensitiveData(string configName, Dictionary data) + { + ConfigName = configName; + Data = data; + } + + public ConfigSensitiveData(string configName) + { + ConfigName = configName; + Data = new Dictionary(); + } + + public void AddConfigValue(string key, string? value) + { + Data.Add(key, value); + } +} + +public static class ConfigSensitiveDataExtensions +{ + public static string? GetConfigValue(this ConfigSensitiveData[] configs, string configName, string fieldName) + { + var config = configs.First(x => x.ConfigName == configName); + config.Data.TryGetValue(fieldName, out string? value); + return value; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs new file mode 100644 index 00000000..ce207e13 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs @@ -0,0 +1,11 @@ +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; + +public class E2ERadiusConfiguration( + RadiusAdapterConfiguration rootConfig, + Dictionary? clientConfigs = null) +{ + public RadiusAdapterConfiguration RootConfiguration { get; } = rootConfig; + public Dictionary? ClientConfigs { get; } = clientConfigs; +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs new file mode 100644 index 00000000..aae56c21 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs @@ -0,0 +1,16 @@ +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.XmlAppConfiguration; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; + +public class RadiusConfigurationModel : RadiusConfigurationSource +{ + public override string Name { get; } + + public RadiusConfigurationModel(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + Name = name; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Radius/RadiusPacketFactory.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Radius/RadiusPacketFactory.cs new file mode 100644 index 00000000..de4c15ac --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/Radius/RadiusPacketFactory.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; +using MultiFactor.Radius.Adapter.Core.Radius; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Radius; + +internal static class RadiusPacketFactory +{ + public static IRadiusPacket? AccessRequest(SharedSecret? packetSecret = null, byte identifier = 0) + { + var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, identifier); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); + var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); + return packet; + } + + public static IRadiusPacket? AccessChallenge(SharedSecret? packetSecret = null, byte identifier = 0) + { + var header = RadiusPacketHeader.Create(PacketCode.AccessChallenge, identifier); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); + var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); + return packet; + } + + public static IRadiusPacket? AccessReject(SharedSecret? packetSecret = null, byte identifier = 0) + { + var header = RadiusPacketHeader.Create(PacketCode.AccessReject, identifier); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); + var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); + return packet; + } + + public static IRadiusPacket? StatusServer(SharedSecret? packetSecret = null, byte identifier = 0) + { + var header = RadiusPacketHeader.Create(PacketCode.StatusServer, identifier); + var sharedSecret = packetSecret ?? new SharedSecret(Convert.ToHexString(GenerateSecret()).ToLower()); + var packet = new RadiusPacket(header, new RadiusAuthenticator(), sharedSecret); + return packet; + } + + private static byte[] GenerateSecret() + { + using var rng = RandomNumberGenerator.Create(); + var data = new byte[16]; + // Fill the salt with cryptographically strong byte values. + rng.GetNonZeroBytes(data); + return data; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..d5fad5c5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures; + +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection RemoveService(this IServiceCollection services) where TService : class + { + services.RemoveAll(); + return services; + } + + public static bool HasDescriptor(this IServiceCollection services) where TService : class + { + return services.FirstOrDefault(x => x.ServiceType == typeof(TService)) != null; + } + + /// + /// Replaces implementation to if the service collection contains descriptor. + /// + /// Abstraction type. + /// Implementation type. + /// Service Collection + /// for chaining. + public static IServiceCollection ReplaceService(this IServiceCollection services) + where TService : class where TImplementation : class, TService + { + var descriptor = services.SingleOrDefault(x => x.ServiceType == typeof(TService)); + if (descriptor == null) return services; + + var newDescriptor = new ServiceDescriptor(typeof(TService), typeof(TImplementation), descriptor.Lifetime); + services.Remove(descriptor); + services.Add(newDescriptor); + + return services; + } + + /// + /// Replaces implementation to the concrete instance of if the service collection contains descriptor. + /// + /// Abstraction type. + /// Service Collection. + /// Implementation instanbce. + /// for chaining. + public static IServiceCollection ReplaceService(this IServiceCollection services, TService instance) + where TService : class + { + var descriptor = services.SingleOrDefault(x => x.ServiceType == typeof(TService)); + if (descriptor == null) return services; + + var newDescriptor = new ServiceDescriptor(typeof(TService), instance); + services.Remove(descriptor); + services.Add(newDescriptor); + + return services; + } + + /// + /// Replaces implementation to the concrete instance of created by the specified factory if the service collection contains descriptor. + /// + /// Abstraction type + /// Service Collection. + /// Implementation instance factory. + /// for chaining. + public static IServiceCollection ReplaceService(this IServiceCollection services, Func factory) + where TService : class + { + var descriptor = services.SingleOrDefault(x => x.ServiceType == typeof(TService)); + if (descriptor == null) return services; + + var newDescriptor = new ServiceDescriptor(typeof(TService), factory, descriptor.Lifetime); + services.Remove(descriptor); + services.Add(newDescriptor); + + return services; + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironment.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironment.cs new file mode 100644 index 00000000..50cf86e9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironment.cs @@ -0,0 +1,39 @@ +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures; + +internal enum TestAssetLocation +{ + RootDirectory, + ClientsDirectory, + E2EBaseConfigs, + E2ESensitiveData +} + +internal static class TestEnvironment +{ + private static readonly string AppFolder = $"{Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)}{Path.DirectorySeparatorChar}"; + private static readonly string AssetsFolder = $"{AppFolder}Assets"; + + public static string GetAssetPath(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return AssetsFolder; + return $"{AssetsFolder}{Path.DirectorySeparatorChar}{fileName}"; + } + + public static string GetAssetPath(TestAssetLocation location) + { + return location switch + { + TestAssetLocation.ClientsDirectory => $"{AssetsFolder}{Path.DirectorySeparatorChar}clients", + TestAssetLocation.E2EBaseConfigs => $"{AssetsFolder}{Path.DirectorySeparatorChar}BaseConfigs", + TestAssetLocation.E2ESensitiveData => $"{AssetsFolder}{Path.DirectorySeparatorChar}SensitiveData", + _ => AssetsFolder, + }; + } + + public static string GetAssetPath(TestAssetLocation location, string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return GetAssetPath(location); + var s = $"{GetAssetPath(location)}{Path.DirectorySeparatorChar}{Path.Combine(fileName.Split('/', '\\'))}"; + return s; + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironmentVariables.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironmentVariables.cs new file mode 100644 index 00000000..80781783 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Fixtures/TestEnvironmentVariables.cs @@ -0,0 +1,71 @@ +namespace Multifactor.Radius.Adapter.EndToEndTests.Fixtures; + +/// +/// Wraps +/// +internal class TestEnvironmentVariables +{ + private readonly HashSet _names; + + private TestEnvironmentVariables(HashSet names) + { + _names = names; + } + + public static void With(Action action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + var names = new HashSet(); + + action(new TestEnvironmentVariables(names)); + + foreach (var name in names) + { + Environment.SetEnvironmentVariable(name, null); + } + } + + public static async Task With(Func action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + var names = new HashSet(); + + await action(new TestEnvironmentVariables(names)); + + foreach (var name in names) + { + Environment.SetEnvironmentVariable(name, null); + } + } + + public TestEnvironmentVariables SetEnvironmentVariable(string name, string value) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + _names.Add(name); + Environment.SetEnvironmentVariable(name, value); + + return this; + } + + public TestEnvironmentVariables SetEnvironmentVariables(Dictionary dictionary) + { + foreach (var env in dictionary) + { + SetEnvironmentVariable(env.Key, env.Value); + } + + return this; + } +} diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Multifactor.Radius.Adapter.EndToEndTests.csproj b/src/Multifactor.Radius.Adapter.EndToEndTests/Multifactor.Radius.Adapter.EndToEndTests.csproj new file mode 100644 index 00000000..4e35422f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Multifactor.Radius.Adapter.EndToEndTests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + + false + true + Linux + + + + + + + + + + + + + ..\libs\LdapForNet.dll + + + + + + + + + + + + + Always + + + diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/RadiusFixtures.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/RadiusFixtures.cs new file mode 100644 index 00000000..2d016407 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/RadiusFixtures.cs @@ -0,0 +1,26 @@ +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Udp; + +namespace Multifactor.Radius.Adapter.EndToEndTests; + +public class RadiusFixtures : IDisposable +{ + public RadiusPacketParser Parser { get; } = E2ETestsUtils.GetRadiusPacketParser(); + + public UdpSocket UdpSocket { get; } = E2ETestsUtils.GetUdpSocket( + RadiusAdapterConstants.LocalHost, + RadiusAdapterConstants.DefaultRadiusAdapterPort); + + public SharedSecret? SharedSecret { get; } = new(RadiusAdapterConstants.DefaultSharedSecret); + + public void Dispose() + { + UdpSocket.Dispose(); + } +} + +[CollectionDefinition("Radius e2e")] +public class RadiusFixturesCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessChallengeTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessChallengeTests.cs new file mode 100644 index 00000000..50dc988f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessChallengeTests.cs @@ -0,0 +1,204 @@ +using System.Text; +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Server; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class AccessChallengeTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("none-root-conf.env")] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST018_ShouldAccept(string configName) + { + var state = "BST018_ShouldAccept"; + var challenge1 = "challenge-1"; + var challenge2 = "challenge-2"; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Awaiting, state)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge1, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Awaiting)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge2, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", RadiusAdapterConstants.BindUserPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + Assert.NotEmpty(response.Attributes["State"]); + var responseState = Encoding.UTF8.GetString((byte[])response.Attributes["State"].First()); + Assert.Equal(responseState, state); + + // Challenge step 2 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge1); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(2, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + + // Challenge step 3 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 2); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge2); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(3, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("none-root-conf.env")] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST019_ShouldAccept(string configName) + { + var state = "BST019_ShouldAccept"; + var challenge1 = "challenge-1"; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Awaiting, state)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge1, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", RadiusAdapterConstants.BindUserPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + Assert.NotEmpty(response.Attributes["State"]); + var responseState = Encoding.UTF8.GetString((byte[])response.Attributes["State"].First()); + Assert.Equal(responseState, state); + + // Challenge step 2 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge1); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(2, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessRequestAttributesTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessRequestAttributesTests.cs new file mode 100644 index 00000000..e42804ec --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/AccessRequestAttributesTests.cs @@ -0,0 +1,167 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models.RadiusReply; +using MultiFactor.Radius.Adapter.Infrastructure.Http; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Dto; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class AccessRequestAttributesTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("none-root-access-request-attributes.env")] + [InlineData("ad-root-access-request-attributes.env")] + [InlineData("radius-root-access-request-attributes.env")] + public async Task BST026_ShouldAcceptAndSendAttributes(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName, "__"); + + var mfAPiMock = new Mock(); + CreateRequestDto payload = null; + mfAPiMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .Callback((CreateRequestDto x, BasicAuthHeaderValue y) => payload = x) + .ReturnsAsync(new AccessRequestDto() { Status = RequestStatus.Granted} ); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + Assert.NotNull(payload); + Assert.NotEmpty(payload.Email); + Assert.NotEmpty(payload.Name); + Assert.NotEmpty(payload.Phone); + } + + [Theory] + [InlineData("none-root-access-request-attributes.env", "Partial:RemoteHost")] + [InlineData("ad-root-access-request-attributes.env", "Partial:RemoteHost")] + [InlineData("radius-root-access-request-attributes.env", "Partial:RemoteHost")] + [InlineData("none-root-access-request-attributes.env", "Full")] + [InlineData("ad-root-access-request-attributes.env", "Full")] + [InlineData("radius-root-access-request-attributes.env", "Full")] + public async Task BST027_ShouldAcceptAndNotSendAttributes(string configName, string privacyMode) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName, "__"); + + var mfAPiMock = new Mock(); + CreateRequestDto payload = null; + mfAPiMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .Callback((CreateRequestDto x, BasicAuthHeaderValue y) => payload = x) + .ReturnsAsync(new AccessRequestDto() { Status = RequestStatus.Granted} ); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, privacyMode: privacyMode); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + Assert.NotNull(payload); + Assert.Null(payload.Email); + Assert.Null(payload.Name); + Assert.Null(payload.Phone); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData, string privacyMode = null) + + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ServiceAccountPassword = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountPassword)), + + ServiceAccountUser = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountUser)), + + PhoneAttribute = "mobile", + PrivacyMode = privacyMode + }, + + RadiusReply = new RadiusReplySection() + { + Attributes = new RadiusReplyAttributesSection(singleElement: new RadiusReplyAttribute() + { Name = "Class", From = "memberOf" }) + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs new file mode 100644 index 00000000..555159f8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs @@ -0,0 +1,244 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Http; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Dto; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class BypassWhenApiUnreachableTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Fact] + public async Task BST001_ShouldAccept() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(); + + await StartHostAsync(rootConfig, configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Fact] + public async Task BST002_ShouldAccept() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(true); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Fact] + public async Task BST003_ShouldAccept() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new MultifactorApiUnreachableException()); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Fact] + public async Task BST004_ShouldReject() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new MultifactorApiUnreachableException()); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(false); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessReject, response.Header.Code); + } + + [Fact] + public async Task BST005_ShouldAccept() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new MultifactorApiUnreachableException()); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(true); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Fact] + public async Task BST006_ShouldAccept() + { + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRootConfig(false); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRootConfig(bool? bypassSecondFactorWhenApiUnreachable = null) + { + return new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + FirstFactorAuthenticationSource = "None", + BypassSecondFactorWhenApiUnreachable = bypassSecondFactorWhenApiUnreachable ?? true + } + }; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ChangePasswordTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ChangePasswordTests.cs new file mode 100644 index 00000000..411c2209 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ChangePasswordTests.cs @@ -0,0 +1,201 @@ +using System.Text; +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Http; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Dto; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class ChangePasswordTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + private static byte _packetId = 0; + + [Theory] + [InlineData("ad-root-change-password-conf.env")] + public async Task BST020_ShouldAccept(string configName) + { + var newPassword = "Qwerty456!"; + var currentPassword = RadiusAdapterConstants.ChangePasswordUserPassword; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName, "__"); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new AccessRequestDto() { Status = RequestStatus.Granted }); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + // Password changing + await ChangePassword( + currentPassword: currentPassword, + newPassword: newPassword, + rootConfig); + + // Rollback + await ChangePassword( + currentPassword: newPassword, + newPassword: currentPassword, + rootConfig); + } + + [Theory] + [InlineData("ad-root-pre-auth-change-password-conf.env")] + public async Task BST022_ShouldAccept(string configName) + { + var newPassword = "Qwerty456!"; + var currentPassword = RadiusAdapterConstants.ChangePasswordUserPassword; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName, "__"); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new AccessRequestDto() { Status = RequestStatus.Granted }); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + // Password changing + await ChangePassword( + currentPassword: currentPassword, + newPassword: newPassword, + rootConfig); + + // Rollback + await ChangePassword( + currentPassword: newPassword, + newPassword: currentPassword, + rootConfig); + } + + private async Task ChangePassword( + string currentPassword, + string newPassword, + RadiusAdapterConfiguration rootConfig) + { + await SetAttributeForUserInCatalogAsync( + RadiusAdapterConstants.ChangePasswordUserName, + rootConfig, + "pwdLastSet", + 0); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.ChangePasswordUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", currentPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + Assert.NotEmpty(response.Attributes["State"]); + var responseState = Encoding.UTF8.GetString((byte[])response.Attributes["State"].First()); + Assert.True(Guid.TryParse(responseState, out Guid state)); + var stateString = state.ToString(); + + // New Password step 2 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", stateString); + accessRequest!.AddAttribute("User-Password", newPassword); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + + // Repeat password step 3 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State",stateString); + accessRequest!.AddAttribute("User-Password", newPassword); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ServiceAccountPassword = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountPassword)), + + ServiceAccountUser = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountUser)), + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + PreAuthenticationMethod = sensitiveData.GetConfigValue(configName, nameof(AppSettingsSection.PreAuthenticationMethod)), + InvalidCredentialDelay = sensitiveData.GetConfigValue(configName, nameof(AppSettingsSection.InvalidCredentialDelay)), + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/DefaultUsersBindTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/DefaultUsersBindTests.cs new file mode 100644 index 00000000..31d0da40 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/DefaultUsersBindTests.cs @@ -0,0 +1,237 @@ +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class DefaultUsersBindTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("root.ad.env")] + [InlineData("root.radius.env")] + public async Task SendAuthRequestWithoutCredentials_RootConfig_ShouldReject(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var clientConfigName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + }; + + await StartHostAsync(rootConfig); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier } }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessReject, response.Header.Code); + } + + [Theory] + [InlineData("root.ad.env")] + [InlineData("root.radius.env")] + public async Task SendAuthRequest_RootConfig_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var clientConfigName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + }; + + await StartHostAsync(rootConfig); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("client.ad.env")] + [InlineData("client.radius.env")] + public async Task SendAuthRequestWithBindUser_ClientConfig_ShouldAccept(string configName) + { + var config = CreateRadiusConfiguration(configName); + await StartHostAsync(config.RootConfiguration, config.ClientConfigs); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("client.ad.env")] + [InlineData("client.radius.env")] + public async Task SendAuthRequestWithAdminUser_ClientConfig_ShouldAccept(string configName) + { + var config = CreateRadiusConfiguration(configName); + await StartHostAsync(config.RootConfiguration, config.ClientConfigs); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.AdminUserName }, + { "User-Password", RadiusAdapterConstants.AdminUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("client.ad.env")] + [InlineData("client.radius.env")] + public async Task SendAuthRequestWithPasswordUser_ClientConfig_ShouldAccept(string configName) + { + var config = CreateRadiusConfiguration(configName); + await StartHostAsync(config.RootConfiguration, config.ClientConfigs); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.ChangePasswordUserName }, + { "User-Password", RadiusAdapterConstants.ChangePasswordUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private E2ERadiusConfiguration CreateRadiusConfiguration(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug" + } + }; + + var clientConfigName = "client"; + var clientConfigs = new Dictionary() + { + { + clientConfigName, new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + clientConfigName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + } + } + }; + + return new E2ERadiusConfiguration(rootConfig, clientConfigs); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/FirstFactorTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/FirstFactorTests.cs new file mode 100644 index 00000000..1fa79803 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/FirstFactorTests.cs @@ -0,0 +1,133 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class FirstFactorTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST016_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST017_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfApiMock = new Mock(); + + mfApiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfApiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", "Bad-Password" } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Empty(mfApiMock.Invocations); + Assert.Equal(PacketCode.AccessReject, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs new file mode 100644 index 00000000..2c4d415e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs @@ -0,0 +1,127 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class MultipleActiveDirectory2FaGroupsTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST012_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group;E2E"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST013_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group-1;Not-Existed-Group-2"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Empty(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration( + ConfigSensitiveData[] sensitiveData, + string activeDirectory2FaGroups) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ActiveDirectory2faGroup = activeDirectory2FaGroups + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs new file mode 100644 index 00000000..ace518cb --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs @@ -0,0 +1,127 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class MultipleActiveDirectoryGroupsTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST009_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group;E2E"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST010_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group-1;Not-Existed-Group-2"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Empty(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessReject, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration( + ConfigSensitiveData[] sensitiveData, + string activeDirectoryGroups) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ActiveDirectoryGroup = activeDirectoryGroups + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/PreSecondFactorTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/PreSecondFactorTests.cs new file mode 100644 index 00000000..7dd73391 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/PreSecondFactorTests.cs @@ -0,0 +1,254 @@ +using System.Text; +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Server; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class PreSecondFactorTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("none-root-conf.env")] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST021_ShouldAccept(string configName) + { + var state = "BST021_ShouldAccept"; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept, state)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", RadiusAdapterConstants.BindUserPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + Assert.False(response.Attributes.ContainsKey("State")); + } + + [Theory] + [InlineData("none-root-conf.env")] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST023_ShouldAccept(string configName) + { + var challenge1 = "challenge-1"; + var state = "BST023_ShouldAccept"; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Awaiting, state)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge1, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", RadiusAdapterConstants.BindUserPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + Assert.NotEmpty(response.Attributes["State"]); + var responseState = Encoding.UTF8.GetString((byte[])response.Attributes["State"].First()); + Assert.Equal(responseState, state); + + // Challenge step 2 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge1); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(2, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("none-root-conf.env")] + [InlineData("ad-root-conf.env")] + [InlineData("radius-root-conf.env")] + public async Task BST024_ShouldAccept(string configName) + { + var state = "BST018_ShouldAccept"; + var challenge1 = "challenge-1"; + var challenge2 = "challenge-2"; + + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Awaiting, state)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge1, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Awaiting)); + + mfAPiMock + .Setup(x => x.ChallengeAsync(It.IsAny(), challenge2, It.IsAny())) + .ReturnsAsync(new ChallengeResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var defaultRequestAttributes = new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName } + }; + + // AccessRequest step 1 + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("User-Password", RadiusAdapterConstants.BindUserPassword); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + Assert.NotEmpty(response.Attributes["State"]); + var responseState = Encoding.UTF8.GetString((byte[])response.Attributes["State"].First()); + Assert.Equal(responseState, state); + + // Challenge step 2 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge1); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(2, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessChallenge, response.Header.Code); + + // Challenge step 3 + accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 2); + accessRequest!.AddAttributes(defaultRequestAttributes); + accessRequest!.AddAttribute("State", state); + accessRequest!.AddAttribute("User-Password", challenge2); + + response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Equal(3, mfAPiMock.Invocations.Count); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + PreAuthenticationMethod = "push", + InvalidCredentialDelay = "3", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)) + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ReplyAttributesTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ReplyAttributesTests.cs new file mode 100644 index 00000000..2edff5e2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/ReplyAttributesTests.cs @@ -0,0 +1,113 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models.RadiusReply; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class ReplyAttributesTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("none-root-reply-attributes.env")] + [InlineData("ad-root-reply-attributes.env")] + [InlineData("radius-root-reply-attributes.env")] + public async Task BST025_ShouldAcceptAndReturnAttributes(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName, "__"); + + var mfAPiMock = new Mock(); + + mfAPiMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(mfAPiMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(mfAPiMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + Assert.NotEmpty(response.Attributes); + Assert.True(response.Attributes.ContainsKey("Class")); + var classAttribute = response.Attributes["Class"]; + Assert.NotEmpty(classAttribute); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + NpsServerEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.NpsServerEndpoint)), + + AdapterClientEndpoint = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.AdapterClientEndpoint)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ServiceAccountPassword = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountPassword)), + + ServiceAccountUser = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ServiceAccountUser)) + }, + + RadiusReply = new RadiusReplySection() + { + Attributes = new RadiusReplyAttributesSection(singleElement: new RadiusReplyAttribute() + { Name = "Class", From = "memberOf" }) + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs new file mode 100644 index 00000000..9ff718b6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs @@ -0,0 +1,127 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class SingleActiveDirectory2FaBypassGroupTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST014_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "E2E"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Empty(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST015_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration( + ConfigSensitiveData[] sensitiveData, + string activeDirectory2FaBypassGroup) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ActiveDirectory2faBypassGroup = activeDirectory2FaBypassGroup + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs new file mode 100644 index 00000000..7319790d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs @@ -0,0 +1,88 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class SingleActiveDirectory2FaGroupTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST011_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "E2E"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration( + ConfigSensitiveData[] sensitiveData, + string groups) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ActiveDirectory2faGroup = groups + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs new file mode 100644 index 00000000..ea0d90a8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs @@ -0,0 +1,127 @@ +using Moq; +using MultiFactor.Radius.Adapter.Core.Framework; +using MultiFactor.Radius.Adapter.Core.Framework.Context; +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi; +using MultiFactor.Radius.Adapter.Services.MultiFactorApi.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class SingleActiveDirectoryGroupTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST007_ShouldAccept(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "E2E"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Single(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + } + + [Theory] + [InlineData("ad-root-conf.env")] + public async Task BST008_ShouldReject(string configName) + { + var sensitiveData = + E2ETestsUtils.GetConfigSensitiveData(configName); + + var secondFactorMock = new Mock(); + + secondFactorMock + .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationCode.Accept)); + + var hostConfiguration = (RadiusHostApplicationBuilder builder) => + { + builder.Services.ReplaceService(secondFactorMock.Object); + }; + + var rootConfig = CreateRadiusConfiguration(sensitiveData, "Not-Existed-Group"); + + await StartHostAsync( + rootConfig, + configure: hostConfiguration); + + var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); + accessRequest!.AddAttributes(new Dictionary() + { + { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier }, + { "User-Name", RadiusAdapterConstants.BindUserName }, + { "User-Password", RadiusAdapterConstants.BindUserPassword } + }); + + var response = SendPacketAsync(accessRequest); + + Assert.NotNull(response); + Assert.Empty(secondFactorMock.Invocations); + Assert.Equal(PacketCode.AccessReject, response.Header.Code); + } + + private RadiusAdapterConfiguration CreateRadiusConfiguration( + ConfigSensitiveData[] sensitiveData, + string activeDirectoryGroup) + { + var configName = "root"; + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + + ActiveDirectoryDomain = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.ActiveDirectoryDomain)), + + FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( + configName, + nameof(AppSettingsSection.FirstFactorAuthenticationSource)), + + ActiveDirectoryGroup = activeDirectoryGroup + } + }; + + return rootConfig; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/StatusServerTests.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/StatusServerTests.cs new file mode 100644 index 00000000..22024309 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Tests/StatusServerTests.cs @@ -0,0 +1,45 @@ +using MultiFactor.Radius.Adapter.Core.Radius; +using Multifactor.Radius.Adapter.EndToEndTests.Constants; +using Multifactor.Radius.Adapter.EndToEndTests.Fixtures.Models; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Models; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Tests; + +[Collection("Radius e2e")] +public class StatusServerTests(RadiusFixtures radiusFixtures) : E2ETestBase(radiusFixtures) +{ + [Fact] + public async Task GetServerStatus_ShouldSuccess() + { + var rootConfig = new RadiusAdapterConfiguration() + { + AppSettings = new AppSettingsSection() + { + AdapterServerEndpoint = "0.0.0.0:1812", + MultifactorApiUrl = "https://api.multifactor.dev", + LoggingLevel = "Debug", + RadiusSharedSecret = RadiusAdapterConstants.DefaultSharedSecret, + RadiusClientNasIdentifier = RadiusAdapterConstants.DefaultNasIdentifier, + BypassSecondFactorWhenApiUnreachable = true, + MultifactorNasIdentifier = "nas-identifier", + MultifactorSharedSecret = "shared-secret", + FirstFactorAuthenticationSource = "None" + } + }; + + await StartHostAsync(rootConfig); + + var serverStatusPacket = CreateRadiusPacket(PacketCode.StatusServer); + + serverStatusPacket!.AddAttributes(new Dictionary() { { "NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier } }); + + var response = SendPacketAsync(serverStatusPacket); + + Assert.NotNull(response); + Assert.Equal(PacketCode.AccessAccept, response.Header.Code); + + var replyMessage = response.Attributes["Reply-Message"].FirstOrDefault()?.ToString(); + Assert.NotNull(replyMessage); + Assert.StartsWith("Server up", replyMessage); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpData.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpData.cs new file mode 100644 index 00000000..ac2120d1 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpData.cs @@ -0,0 +1,25 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Udp; + +public record UdpData +{ + private readonly Memory _memory; + private byte[]? _bytes; + private string? _string; + + public UdpData(Memory bytes) + { + _memory = bytes; + } + + public byte[] GetBytes() + { + return _bytes ??= _memory.ToArray(); + } + + public string GetString() + { + return _string ??= Encoding.ASCII.GetString(GetBytes()); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpSocket.cs b/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpSocket.cs new file mode 100644 index 00000000..3d084b91 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.EndToEndTests/Udp/UdpSocket.cs @@ -0,0 +1,44 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Multifactor.Radius.Adapter.EndToEndTests.Udp; + +public class UdpSocket : IDisposable +{ + private readonly IPEndPoint _endPoint; + private readonly Socket _socket; + private const int MaxUdpSize = 65_535; + + public UdpSocket(IPAddress ip, int port) + { + _endPoint = new IPEndPoint(ip, port); + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); + } + + public void Send(string data) + { + var bytes = Encoding.ASCII.GetBytes(data); + Send(bytes); + } + + public void Send(byte[] data) + { + _socket.SendTo(data, _endPoint); + } + + public UdpData Receive() + { + var buffer = new byte[MaxUdpSize]; + var ep = (EndPoint)_endPoint; + var received = _socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref ep); + var m = new Memory(buffer, 0, received); + return new UdpData(m); + } + + public void Dispose() + { + _socket.Dispose(); + } +} \ No newline at end of file diff --git a/src/multifactor-radius-adapter.sln b/src/multifactor-radius-adapter.sln index 8b6a2a28..50af90b4 100644 --- a/src/multifactor-radius-adapter.sln +++ b/src/multifactor-radius-adapter.sln @@ -9,12 +9,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\LICENSE.ru.md = ..\LICENSE.ru.md ..\README.md = ..\README.md ..\README.ru.md = ..\README.ru.md + testenvironments.json = testenvironments.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiFactor.Radius.Adapter.Tests", "MultiFactor.Radius.Adapter.Tests\MultiFactor.Radius.Adapter.Tests.csproj", "{E8A7518C-A622-4343-A594-46EE5869EE96}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiFactor.Radius.Adapter", "MultiFactor.Radius.Adapter\MultiFactor.Radius.Adapter.csproj", "{8C663BCC-03FE-437C-A81E-1B581BF2BD3D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Multifactor.Radius.Adapter.EndToEndTests", "Multifactor.Radius.Adapter.EndToEndTests\Multifactor.Radius.Adapter.EndToEndTests.csproj", "{23295204-C87C-433C-A5A7-2085E961E126}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +32,10 @@ Global {8C663BCC-03FE-437C-A81E-1B581BF2BD3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C663BCC-03FE-437C-A81E-1B581BF2BD3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C663BCC-03FE-437C-A81E-1B581BF2BD3D}.Release|Any CPU.Build.0 = Release|Any CPU + {23295204-C87C-433C-A5A7-2085E961E126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23295204-C87C-433C-A5A7-2085E961E126}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23295204-C87C-433C-A5A7-2085E961E126}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23295204-C87C-433C-A5A7-2085E961E126}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/testenvironments.json b/src/testenvironments.json new file mode 100644 index 00000000..46e2fb87 --- /dev/null +++ b/src/testenvironments.json @@ -0,0 +1,10 @@ +{ + "version": "1", // value must be 1 + "environments": [ + { + "name": "Linux x64", + "type": "docker", + "dockerFile": "Multifactor.Radius.Adapter.EndToEndTests\\Dockerfile" + } + ] +} \ No newline at end of file