diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs new file mode 100644 index 00000000..9085699c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs @@ -0,0 +1,7 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Cache; + +public interface IAuthenticatedClientCache +{ + void SetCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime); + bool TryHitCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Cache/ICacheService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs similarity index 61% rename from src/Multifactor.Radius.Adapter.v2/Services/Cache/ICacheService.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs index 90349d5c..85ed7dec 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Cache/ICacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs @@ -1,9 +1,9 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Cache; +namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface ICacheService { + //TODO разделить на несколько void Set(string key, T value, DateTimeOffset expirationDate); - void Set(string key, T value); bool TryGetValue(string key, out T? value); void Remove(string key); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/ApplicationVariables.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ApplicationVariables.cs similarity index 77% rename from src/Multifactor.Radius.Adapter.v2/Core/ApplicationVariables.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ApplicationVariables.cs index 70f3cc3d..0adcddfc 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/ApplicationVariables.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ApplicationVariables.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models { public class ApplicationVariables { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs new file mode 100644 index 00000000..55e1131a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs @@ -0,0 +1,36 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Core.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface IClientConfiguration +{ + public string Name { get; } + + public string MultifactorNasIdentifier { get; } + public string MultifactorSharedSecret { get; } + public IReadOnlyList SignUpGroups { get; } + public bool BypassSecondFactorWhenApiUnreachable { get; } + public AuthenticationSource FirstFactorAuthenticationSource { get; } + public IPEndPoint AdapterClientEndpoint { get; } + + public IReadOnlyList RadiusClientIps { get; } + public string RadiusClientNasIdentifier { get; } + public string RadiusSharedSecret { get; } + public IReadOnlyList NpsServerEndpoints { get; } + public TimeSpan NpsServerTimeout { get; } + + public Privacy Privacy { get; } + + public PreAuthMode? PreAuthenticationMethod { get; } + public TimeSpan AuthenticationCacheLifetime { get; } + public CredentialDelay? InvalidCredentialDelay { get; } + public string? CallingStationIdAttribute { get; } //TODO not used + public IReadOnlyList IpWhiteList { get; } + + public IReadOnlyList? LdapServers { get; } + public IReadOnlyDictionary>? ReplyAttributes { get; } +} + diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs new file mode 100644 index 00000000..b80df211 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs @@ -0,0 +1,27 @@ +using Multifactor.Core.Ldap.Name; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface ILdapServerConfiguration +{ + public string ConnectionString { get; } + public string Username { get; } + public string Password { get; } + public int BindTimeoutSeconds{ get; } + public IReadOnlyList AccessGroups { get; } + public IReadOnlyList SecondFaGroups { get; } + public IReadOnlyList SecondFaBypassGroups { get; } + public bool LoadNestedGroups { get; } + public IReadOnlyList NestedGroupsBaseDns { get; } + public IReadOnlyList AuthenticationCacheGroups { get; } + public IReadOnlyList PhoneAttributes { get; } + public string IdentityAttribute { get; } + public bool RequiresUpn { get; } + public bool TrustedDomainsEnabled { get; } + public bool AlternativeSuffixesEnabled { get; } + public IReadOnlyList IncludedDomains { get; }//TODO not used + public IReadOnlyList ExcludedDomains { get; }//TODO not used + public IReadOnlyList IncludedSuffixes { get; } + public IReadOnlyList ExcludedSuffixes { get; } + public IReadOnlyList BypassSecondFactorWhenApiUnreachableGroups { get; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs new file mode 100644 index 00000000..dac3b8f9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs @@ -0,0 +1,12 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface IRadiusReplyAttribute +{ + public string Name { get; } + public object Value { get; } + public IReadOnlyList UserGroupCondition { get; } + public IReadOnlyList UserNameCondition { get; } + public bool Sufficient { get; } + public bool IsMemberOf => Name?.ToLower() == "memberof"; + public bool FromLdap => !string.IsNullOrWhiteSpace(Name); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs new file mode 100644 index 00000000..a2aaee90 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs @@ -0,0 +1,24 @@ +using System.Net; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface IRootConfiguration +{ + IReadOnlyList MultifactorApiUrls { get; } + string? MultifactorApiProxy { get; } + TimeSpan MultifactorApiTimeout { get; } + IPEndPoint? AdapterServerEndpoint { get; } + string LoggingLevel { get; } + string? LoggingFormat { get; } + bool SyslogUseTls { get; } + string? SyslogServer { get; } + string? SyslogFormat { get; } + string? SyslogFacility { get; } + string SyslogAppName { get; } + string? SyslogFramer { get; } + string? SyslogOutputTemplate { get; } + + string? ConsoleLogOutputTemplate { get; } + string? FileLogOutputTemplate { get; } + int LogFileMaxSizeBytes { get; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs new file mode 100644 index 00000000..9a02ffd6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -0,0 +1,22 @@ +using System.Net; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class ServiceConfiguration +{ + public required IRootConfiguration RootConfiguration { get; init; } + public required IReadOnlyList ClientsConfigurations { get; init; } + public IClientConfiguration? GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientNasIdentifier == nasIdentifier); + public IClientConfiguration? GetClientConfiguration(IPAddress ip) + { + if (SingleClientMode) + { + return ClientsConfigurations.FirstOrDefault(); + } + + return ClientsConfigurations.FirstOrDefault(config => + config.RadiusClientIps != null && config.RadiusClientIps.Any() && config.RadiusClientIps.Contains(ip)); + + } + public bool SingleClientMode { get; init; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/CredentialDelay.cs b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/CredentialDelay.cs new file mode 100644 index 00000000..36f6e84d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/CredentialDelay.cs @@ -0,0 +1,4 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Core.Models +{ + public record CredentialDelay(int Min, int Max); +} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/AuthenticationSource.cs similarity index 62% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/AuthenticationSource.cs index ae3e6cf3..e6b45086 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/AuthenticationSource.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth +namespace Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum { [Flags] public enum AuthenticationSource diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PreAuthMode.cs similarity index 79% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PreAuthMode.cs index 0bb539f7..b7889cb9 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PreAuthMode.cs @@ -1,5 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode +namespace Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum { + [Flags] public enum PreAuthMode { /// diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PrivacyMode.cs similarity index 86% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PrivacyMode.cs index d4e1f5a9..b497df42 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Enum/PrivacyMode.cs @@ -2,11 +2,12 @@ //Please see licence at //https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; +namespace Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; /// /// User information disclosure mode /// +[Flags] public enum PrivacyMode { /// diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Privacy.cs b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Privacy.cs new file mode 100644 index 00000000..1b61fe3e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Core/Models/Privacy.cs @@ -0,0 +1,6 @@ +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Core.Models +{ + public record Privacy(PrivacyMode PrivacyMode, string[] PrivacyFields); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs new file mode 100644 index 00000000..7cd8d370 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +namespace Multifactor.Radius.Adapter.v2.Application.Extensions; + +public static class ApplicationExtensions +{ + public static void AddApplicationVariables(this IServiceCollection services) + { + var appVars = new ApplicationVariables + { + AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), + AppVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(), + StartedAt = DateTime.Now + }; + services.AddSingleton(appVars); + } + + private static void AddLdapBindNameFormation(IServiceCollection services) + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public static void AddFirstFactor(this IServiceCollection services) + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public static void AddChallenge(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + } + + public static void AddPipelines(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddPipelineSteps(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public static void AddAppServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + AddLdapBindNameFormation(services); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs new file mode 100644 index 00000000..396d997c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs @@ -0,0 +1,12 @@ +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class ChangeUserPasswordRequest +{ + public LdapConnectionData ConnectionData { get; set; } + public ILdapSchema LdapSchema { get; set; } + public DistinguishedName DistinguishedName { get; set; } + public string NewPassword { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs new file mode 100644 index 00000000..c4c59efb --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs @@ -0,0 +1,15 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class FindUserRequest +{ + public LdapConnectionData ConnectionData { get; set; } + public UserIdentity UserIdentity { get; set; } + public DistinguishedName SearchBase { get; set; } + public ILdapSchema LdapSchema { get; set; } + public LdapAttributeName[]? AttributeNames { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapProfile.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ILdapProfile.cs similarity index 82% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapProfile.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ILdapProfile.cs index bb09919b..7c7d0c7a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapProfile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ILdapProfile.cs @@ -1,12 +1,11 @@ using Multifactor.Core.Ldap.Attributes; using Multifactor.Core.Ldap.Name; -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public interface ILdapProfile { DistinguishedName Dn { get; } - string? Upn { get; } string? Phone { get; } string? Email { get; } string? DisplayName { get; } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs new file mode 100644 index 00000000..2f5433e6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class LdapConnectionData +{ + public string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapProfile.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs similarity index 82% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapProfile.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs index 8f4428c0..db4d26e4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapProfile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs @@ -1,10 +1,9 @@ using Multifactor.Core.Ldap.Attributes; using Multifactor.Core.Ldap.Entry; -using Multifactor.Core.Ldap.LangFeatures; using Multifactor.Core.Ldap.Name; using Multifactor.Core.Ldap.Schema; -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public class LdapProfile : ILdapProfile { @@ -12,7 +11,7 @@ public class LdapProfile : ILdapProfile public LdapProfile(LdapEntry ldapEntry, ILdapSchema? schema = null) { - Throw.IfNull(ldapEntry, nameof(ldapEntry)); + ArgumentNullException.ThrowIfNull(ldapEntry, nameof(ldapEntry)); _ldapEntry = ldapEntry; MemberOf = _ldapEntry.Attributes["memberOf"]?.GetNotEmptyValues().Select(n => new DistinguishedName(n, schema)).ToList() ?? []; @@ -26,9 +25,9 @@ public LdapProfile(LdapEntry ldapEntry, ILdapSchema? schema = null) public DistinguishedName Dn { get; } public string? Upn { get; } - public string? Phone { get; } - public string? Email { get; } - public string? DisplayName { get; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? DisplayName { get; set; } public IReadOnlyCollection MemberOf { get; } public IReadOnlyCollection Attributes { get; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs new file mode 100644 index 00000000..60e21c34 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs @@ -0,0 +1,13 @@ +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class LoadUserGroupRequest +{ + public LdapConnectionData ConnectionData { get; set; } + public ILdapSchema LdapSchema { get; set; } + public DistinguishedName UserDN { get; set; } + public DistinguishedName? SearchBase { get; set; } + public int Limit { get; set; } = int.MaxValue; +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs new file mode 100644 index 00000000..f1176bfc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs @@ -0,0 +1,35 @@ +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class MembershipRequest +{ + public LdapConnectionData ConnectionData { get; set; } + public ILdapSchema LdapSchema { get; set; } + public DistinguishedName DistinguishedName { get; set; } + public DistinguishedName[] TargetGroups { get; set; } + public DistinguishedName[] NestedGroupsBaseDns { get; set; } + + public static MembershipRequest FromContext(RadiusPipelineContext context, IReadOnlyList groups) + { + if (groups.Count == 0) + throw new ArgumentNullException(); + + return new MembershipRequest + { + ConnectionData = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, + }, + LdapSchema = context.LdapSchema, + DistinguishedName = context.LdapProfile.Dn, + TargetGroups = groups.ToArray(), + NestedGroupsBaseDns = context.LdapConfiguration.NestedGroupsBaseDns.ToArray() + }; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs new file mode 100644 index 00000000..212ea1cc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs @@ -0,0 +1,14 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; + +public interface ILdapAdapter +{ + IReadOnlyList LoadUserGroups(LoadUserGroupRequest request); + bool IsMemberOf(MembershipRequest request); + ILdapProfile? FindUserProfile(FindUserRequest request); + bool ChangeUserPassword(ChangeUserPasswordRequest request); + ILdapSchema? LoadSchema(LdapConnectionData request); + bool CheckConnection(LdapConnectionData request); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs new file mode 100644 index 00000000..47fa0db8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs @@ -0,0 +1,18 @@ +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md + + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions +{ + [Serializable] + public class MultifactorApiUnreachableException : Exception + { + public MultifactorApiUnreachableException() { } + public MultifactorApiUnreachableException(string message) : base(message) { } + public MultifactorApiUnreachableException(string message, Exception inner) : base(message, inner) { } + protected MultifactorApiUnreachableException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs similarity index 60% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequest.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs index d4be783f..0155d9fd 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs @@ -1,6 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; -public class AccessRequest +public class AccessRequestQuery { public string? Identity { get; set; } public string? Name { get; set; } @@ -9,6 +9,5 @@ public class AccessRequest public string? PassCode { get; set; } public string? CallingStationId { get; set; } public string? CalledStationId { get; set; } - public Capabilities? Capabilities { get; set; } - public GroupPolicyPreset? GroupPolicyPreset { get; set; } + public string SignUpGroups { get; set; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequestResponse.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs similarity index 83% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequestResponse.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs index 76155f58..d6994bf4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequestResponse.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs @@ -1,4 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; public class AccessRequestResponse { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/ChallengeRequestQuery.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/ChallengeRequestQuery.cs new file mode 100644 index 00000000..7fe57d96 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/ChallengeRequestQuery.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +public class ChallengeRequestQuery +{ + public string Identity { get; set; } + public string Challenge { get; set; } + public string RequestId { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/Enum/RequestStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/Enum/RequestStatus.cs new file mode 100644 index 00000000..872d8863 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/Enum/RequestStatus.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum RequestStatus +{ + AwaitingAuthentication, + Granted, + Denied +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/MultifactorAuthData.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/MultifactorAuthData.cs new file mode 100644 index 00000000..7ec6bbf4 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/MultifactorAuthData.cs @@ -0,0 +1,15 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +public class MultifactorAuthData +{ + public string ApiKey { get; set; } + public string ApiSecret { get; set; } + + public MultifactorAuthData(string apiKey, string apiSecret) + { + ArgumentNullException.ThrowIfNull(apiKey); + ArgumentNullException.ThrowIfNull(apiSecret); + ApiKey = apiKey; + ApiSecret = apiSecret; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs new file mode 100644 index 00000000..c48dbf95 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs @@ -0,0 +1,15 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +public class SecondFactorResponse { + public AuthenticationStatus Code { get; } + public string? ReplyMessage { get; } + public string? State { get; } + public SecondFactorResponse(AuthenticationStatus code, string? state = null, string? replyMessage = null) + { + Code = code; + ReplyMessage = replyMessage; + State = state; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs new file mode 100644 index 00000000..9cf0e29c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs @@ -0,0 +1,342 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; + +public sealed class MultifactorApiService +{ + private readonly IMultifactorApi _api; + private readonly IAuthenticatedClientCache _authenticatedClientCache; + private readonly ILogger _logger; + + public MultifactorApiService( + IMultifactorApi api, + IAuthenticatedClientCache authenticatedClientCache, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(api, nameof(api)); + ArgumentNullException.ThrowIfNull(authenticatedClientCache, nameof(authenticatedClientCache)); + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); + _api = api; + _authenticatedClientCache = authenticatedClientCache; + _logger = logger; + } + + public async Task CreateSecondFactorRequestAsync(RadiusPipelineContext context, bool cacheEnabled) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + _logger.LogInformation($"Creating second-factor request for user {context.RequestPacket.UserName}"); + var personalData = RequestDataExtractor.ExtractPersonalData(context); + if (string.IsNullOrWhiteSpace(personalData.Identity)) + { + _logger.LogWarning("Empty user name for second factor context. Request rejected."); + return new SecondFactorResponse(AuthenticationStatus.Reject); + } + + if (_authenticatedClientCache.TryHitCache( + personalData.CallingStationId, + personalData.Identity, + context.ClientConfiguration.Name, + context.ClientConfiguration.AuthenticationCacheLifetime)) + { + _logger.LogInformation( + "Bypass second factor for user '{user:l}' with calling-station-id {csi:l} from {host:l}:{port}", + personalData.Identity, + personalData.CallingStationId, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + return new SecondFactorResponse(AuthenticationStatus.Bypass); + } + + ApplyPrivacyMode(ref personalData, context.ClientConfiguration.Privacy.PrivacyMode, context.ClientConfiguration.Privacy.PrivacyFields); + + try + { + var request = CreateAccessRequestQuery(personalData, context); + var authData = new MultifactorAuthData( + context.ClientConfiguration.MultifactorNasIdentifier, + context.ClientConfiguration.MultifactorSharedSecret); + + var response = await _api.CreateAccessRequest(request, authData); + var responseCode = ConvertToAuthCode(response); + + if (responseCode == AuthenticationStatus.Reject) + { + var reason = response?.ReplyMessage; + var phone2 = response?.Phone; + _logger.LogWarning( + "Second factor verification for user '{user:l}' from {host:l}:{port} failed with reason='{reason:l}'. User phone {phone:l}", + personalData.Identity, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, + reason, + phone2); + } + + var mfResponse = new SecondFactorResponse(responseCode, state: response?.Id, replyMessage: response?.ReplyMessage); + + if (!ShouldCacheResponse(cacheEnabled, responseCode, response)) + { + _logger.LogDebug("Skip 2FA response caching for user '{user}'.", context.RequestPacket.UserName); + return mfResponse; + } + + LogGrantedInfo(personalData.Identity, response, context.RequestPacket.CallingStationIdAttribute); + _authenticatedClientCache.SetCache( + personalData.CallingStationId, + personalData.Identity, + context.ClientConfiguration.Name, + context.ClientConfiguration.AuthenticationCacheLifetime); + + return mfResponse; + } + catch (MultifactorApiUnreachableException apiEx) + { + return ProcessMfException(apiEx, personalData.Identity, + context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, + context.LdapConfiguration?.BypassSecondFactorWhenApiUnreachableGroups, + context.UserGroups, context.RequestPacket.RemoteEndpoint); + } + catch (Exception ex) + { + return ProcessException(ex, personalData.Identity, context.RequestPacket.RemoteEndpoint); + } + } + + public async Task SendChallengeAsync(RadiusPipelineContext context, bool cacheEnabled, string requestId, string answer) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + ArgumentException.ThrowIfNullOrWhiteSpace(requestId, nameof(requestId)); + ArgumentException.ThrowIfNullOrWhiteSpace(answer, nameof(answer)); + + var identity = RequestDataExtractor.GetSecondFactorIdentity(context); + if (string.IsNullOrWhiteSpace(identity)) + throw new InvalidOperationException("The identity is empty."); + + var dto = new ChallengeRequestQuery + { + Identity = identity, + Challenge = answer, + RequestId = requestId + }; + + var callingStationIdAttr = context.RequestPacket.CallingStationIdAttribute; + var callingStationId = RequestDataExtractor.GetCallingStationId(callingStationIdAttr, context.RequestPacket.RemoteEndpoint); + + try + { + var authData = new MultifactorAuthData( + context.ClientConfiguration.MultifactorNasIdentifier, + context.ClientConfiguration.MultifactorSharedSecret); + + var response = await _api.SendChallengeAsync(dto, authData); + var responseCode = ConvertToAuthCode(response); + + var mfResponse = new SecondFactorResponse(responseCode, state: response?.Id, replyMessage: response?.ReplyMessage); + + if (!ShouldCacheResponse(cacheEnabled, responseCode, response)) + { + _logger.LogDebug("Skip challenge response caching for user '{user}'.", context.RequestPacket.UserName); + return mfResponse; + } + + LogGrantedInfo(identity, response, callingStationId); + _authenticatedClientCache.SetCache( + callingStationId, + identity, + context.ClientConfiguration.Name, + context.ClientConfiguration.AuthenticationCacheLifetime); + + return mfResponse; + } + catch (MultifactorApiUnreachableException apiEx) + { + return ProcessMfException(apiEx, identity, + context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, + context.LdapConfiguration?.BypassSecondFactorWhenApiUnreachableGroups, + context.UserGroups, context.RequestPacket.RemoteEndpoint); + } + catch (Exception ex) + { + return ProcessException(ex, identity, context.RequestPacket.RemoteEndpoint); + } + } + + private AuthenticationStatus ConvertToAuthCode(AccessRequestResponse? multifactorAccessRequest) + { + if (multifactorAccessRequest == null) + return AuthenticationStatus.Reject; + + switch (multifactorAccessRequest.Status) + { + case RequestStatus.Granted when multifactorAccessRequest.Bypassed: + return AuthenticationStatus.Bypass; + + case RequestStatus.Granted: + return AuthenticationStatus.Accept; + + case RequestStatus.Denied: + return AuthenticationStatus.Reject; + + case RequestStatus.AwaitingAuthentication: + return AuthenticationStatus.Awaiting; + + default: + _logger.LogWarning("Got unexpected status from API: {status:l}", multifactorAccessRequest.Status); + return AuthenticationStatus.Reject; + } + } + + private void LogGrantedInfo(string identity, AccessRequestResponse? response, string? callingStationIdAttribute) + { + string? countryValue = null; + string? regionValue = null; + string? cityValue = null; + var callingStationId = callingStationIdAttribute; + + if (response != null && IPAddress.TryParse(callingStationId, out var ip)) + { + countryValue = response.CountryCode; + regionValue = response.Region; + cityValue = response.City; + callingStationId = ip.ToString(); + } + + _logger.LogInformation( + "Second factor for user '{user:l}' verified successfully. Authenticator: '{authenticator:l}', account: '{account:l}', country: '{country:l}', region: '{region:l}', city: '{city:l}', calling-station-id: {clientIp}, authenticatorId: {authenticatorId}", + identity, + response?.Authenticator, + response?.Account, + countryValue, + regionValue, + cityValue, + callingStationId, + response?.AuthenticatorId); + } + + private AccessRequestQuery CreateAccessRequestQuery(PersonalData personalData, RadiusPipelineContext context) + { + var phone = RequestDataExtractor.GetUserPhone(context); + + return new AccessRequestQuery + { + Identity = personalData.Identity, + Name = personalData.DisplayName, + Email = personalData.Email, + Phone = string.IsNullOrWhiteSpace(phone) ? personalData.Phone : phone, + CalledStationId = personalData.CalledStationId, + CallingStationId = personalData.CallingStationId, + SignUpGroups = string.Join(';', context.ClientConfiguration.SignUpGroups), + PassCode = GetPassCodeOrNull(context) + }; + } + + private static string? GetPassCodeOrNull(RadiusPipelineContext context) + { + //check static challenge + var challenge = context.RequestPacket.TryGetChallenge(); + if (challenge != null) + { + return challenge; + } + + //check password challenge (otp or passcode) + var passphrase = context.Passphrase; + switch (context.ClientConfiguration.PreAuthenticationMethod) + { + case PreAuthMode.Otp: + return passphrase.Otp; + } + + if (passphrase.IsEmpty) + return null; + + if (context.ClientConfiguration.FirstFactorAuthenticationSource != AuthenticationSource.None) + return null; + + return passphrase.Otp ?? passphrase.ProviderCode; + } + + private static void ApplyPrivacyMode(ref PersonalData pd, PrivacyMode mode, string[] privacyFields) + { + switch (mode) + { + case PrivacyMode.Full: + pd.DisplayName = null; + pd.Email = null; + pd.Phone = null; + pd.CallingStationId = ""; + pd.CalledStationId = null; + break; + + case PrivacyMode.Partial: + if (!privacyFields.Contains("Name")) + pd.DisplayName = null; + + if (!privacyFields.Contains("Email")) + pd.Email = null; + + if (!privacyFields.Contains("Phone")) + pd.Phone = null; + + if (!privacyFields.Contains("RemoteHost")) + pd.CallingStationId = ""; + + pd.CalledStationId = null; + break; + } + } + + private SecondFactorResponse ProcessMfException( + MultifactorApiUnreachableException apiEx, + string identity, + bool bypassSecondFactorWhenApiUnreachable, + IReadOnlyList bypassSecondFactorWhenApiUnreachableGroups, + HashSet userGroups, + IPEndPoint remoteEndpoint) + { + _logger.LogError(apiEx, + "Error occured while requesting API for user '{user:l}' from {host:l}:{port}, {msg:l}", + identity, + remoteEndpoint.Address, + remoteEndpoint.Port, + apiEx.Message); + + if (bypassSecondFactorWhenApiUnreachable && + (!bypassSecondFactorWhenApiUnreachableGroups.Any() + || bypassSecondFactorWhenApiUnreachableGroups.Intersect(userGroups).Any()) + ) + { + var code = ConvertToAuthCode(AccessRequestResponse.Bypass); + return new SecondFactorResponse(code); + } + + var radCode = ConvertToAuthCode(null); + return new SecondFactorResponse(radCode); + } + + private SecondFactorResponse ProcessException(Exception ex, string identity, IPEndPoint remoteEndpoint) + { + _logger.LogError(ex, + "Error occured while requesting API for user '{user:l}' from {host:l}:{port}, {msg:l}", + identity, + remoteEndpoint.Address, + remoteEndpoint.Port, + ex.Message); + + var code = ConvertToAuthCode(null); + return new SecondFactorResponse(code); + } + + private static bool ShouldCacheResponse(bool apiResponseCacheEnabled, AuthenticationStatus responseCode, AccessRequestResponse? response) + => apiResponseCacheEnabled && responseCode == AuthenticationStatus.Accept && !(response?.Bypassed ?? false); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs new file mode 100644 index 00000000..869e1674 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; + +public interface IMultifactorApi +{ + Task CreateAccessRequest(AccessRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken = default); + Task SendChallengeAsync(ChallengeRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs new file mode 100644 index 00000000..e1119f4c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs @@ -0,0 +1,59 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; + +public static class RequestDataExtractor +{ + public static PersonalData ExtractPersonalData(RadiusPipelineContext context) + { + var identity = GetSecondFactorIdentity(context); + var callingStationId = GetCallingStationId( + context.RequestPacket.CallingStationIdAttribute, + context.RequestPacket.RemoteEndpoint); + + return new PersonalData + { + Identity = identity, + DisplayName = context.LdapProfile?.DisplayName, + Email = context.LdapProfile?.Email, + Phone = GetUserPhone(context), + CalledStationId = context.RequestPacket.CalledStationIdAttribute, + CallingStationId = callingStationId ?? string.Empty + }; + } + + public static string? GetSecondFactorIdentity(RadiusPipelineContext context) + { + return string.IsNullOrWhiteSpace(context.LdapConfiguration?.IdentityAttribute) ? context.RequestPacket.UserName + : context.LdapProfile?.Attributes?.Where(attr => attr.Name == context.LdapConfiguration.IdentityAttribute) + .SelectMany(attr => attr.Values) + .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); + } + + public static string? GetUserPhone(RadiusPipelineContext context) + { + if (context.LdapProfile?.Attributes == null || + context.LdapConfiguration?.PhoneAttributes == null) + return context.LdapProfile?.Phone; + + foreach (var attribute in context.LdapProfile.Attributes) + { + if (!context.LdapConfiguration.PhoneAttributes.Contains(attribute.Name.Value)) continue; + foreach (var value in attribute.GetNotEmptyValues()) + { + if (!string.IsNullOrEmpty(value)) + return value; + } + } + return context.LdapProfile.Phone; + } + + public static string? GetCallingStationId(string? callingStationIdAttributeValue, IPEndPoint remoteEndPoint) + { + return IPAddress.TryParse(callingStationIdAttributeValue ?? string.Empty, out _) + ? callingStationIdAttributeValue + : remoteEndPoint.Address.ToString(); + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs similarity index 73% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs index de237f7d..6c65b72f 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs @@ -1,4 +1,7 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public class ChallengeProcessorProvider : IChallengeProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs new file mode 100644 index 00000000..cd8a53c5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Security; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; + +public class ChangePasswordChallengeProcessor : IChallengeProcessor +{ + private readonly ICacheService _cache; + private readonly ILdapAdapter _ldapAdapter; + private readonly ILogger _logger; + + public ChangePasswordChallengeProcessor( + ICacheService cache, + ILdapAdapter ldapAdapter, + ILogger logger) + { + _cache = cache; + _ldapAdapter = ldapAdapter; + _logger = logger; + } + + public ChallengeType ChallengeType => ChallengeType.PasswordChange; + + public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) + { + ArgumentNullException.ThrowIfNull(context); + if (string.IsNullOrWhiteSpace(context.Passphrase?.Password)) + throw new InvalidOperationException("User password is required."); + + if (string.IsNullOrWhiteSpace(context.MustChangePasswordDomain)) + throw new InvalidOperationException("Domain is required."); + + var encryptedPassword = ProtectionService.Protect(context.ClientConfiguration.MultifactorSharedSecret, context.Passphrase.Password); + + var passwordRequest = new PasswordChangeCache + { + Domain = context.MustChangePasswordDomain, + CurrentPasswordEncryptedData = encryptedPassword + }; + + _cache.Set(passwordRequest.Id, passwordRequest, DateTimeOffset.UtcNow.AddMinutes(5)); + _logger.LogInformation($"Password change state: \"{passwordRequest.Id}\""); + context.ResponseInformation.State = passwordRequest.Id; + context.ResponseInformation.ReplyMessage = "Please change password to continue. Enter new password: "; + return new ChallengeIdentifier(context.ClientConfiguration.Name, context.ResponseInformation.State); + } + + public bool HasChallengeContext(ChallengeIdentifier identifier) => _cache.TryGetValue(identifier.RequestId, out _); + + public Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context.LdapProfile); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration); + ArgumentNullException.ThrowIfNull(context.LdapSchema); + + var passwordChangeRequest = GetPasswordChangeRequest(identifier.RequestId); + if (passwordChangeRequest == null) + return Task.FromResult(ChallengeStatus.Accept); + + if (string.IsNullOrWhiteSpace(context.Passphrase.Raw)) + { + context.ResponseInformation.ReplyMessage = "Password is empty"; + context.FirstFactorStatus = AuthenticationStatus.Reject; + return Task.FromResult(ChallengeStatus.Reject); + } + + if (string.IsNullOrWhiteSpace(passwordChangeRequest.NewPasswordEncryptedData)) + return Task.FromResult(RepeatPasswordChallenge(context, passwordChangeRequest)); + + var decryptedNewPassword = ProtectionService.Unprotect(context.ClientConfiguration.MultifactorSharedSecret, passwordChangeRequest.NewPasswordEncryptedData); + if (decryptedNewPassword != context.Passphrase.Raw) + return Task.FromResult(PasswordsNotMatchChallenge(context, passwordChangeRequest)); + + var request = new ChangeUserPasswordRequest + { + ConnectionData = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }, + LdapSchema = context.LdapSchema, + DistinguishedName = context.LdapProfile.Dn, + NewPassword = decryptedNewPassword, + }; + + var success = _ldapAdapter.ChangeUserPassword(request); + + _cache.Remove(passwordChangeRequest.Id); + context.ResponseInformation.State = null; + + if (success) + return Task.FromResult(ChallengeStatus.Accept); + + context.FirstFactorStatus = AuthenticationStatus.Reject; + + return Task.FromResult(ChallengeStatus.Reject); + } + + private PasswordChangeCache? GetPasswordChangeRequest(string id) + { + _cache.TryGetValue(id, out PasswordChangeCache? passwordChangeRequest); + return passwordChangeRequest; + } + + private ChallengeStatus PasswordsNotMatchChallenge(RadiusPipelineContext request, PasswordChangeCache passwordChangeRequest) + { + passwordChangeRequest.NewPasswordEncryptedData = null; + + _cache.Set(passwordChangeRequest.Id, passwordChangeRequest, DateTimeOffset.UtcNow.AddMinutes(5)); + + request.ResponseInformation.State = passwordChangeRequest.Id; + request.ResponseInformation.ReplyMessage = "Passwords not match. Please enter new password: "; + + return ChallengeStatus.InProcess; + } + + private ChallengeStatus RepeatPasswordChallenge(RadiusPipelineContext context, PasswordChangeCache passwordChangeRequest) + { + passwordChangeRequest.NewPasswordEncryptedData = ProtectionService.Protect(context.ClientConfiguration.MultifactorSharedSecret, context.Passphrase.Raw!); + + _cache.Set(passwordChangeRequest.Id, passwordChangeRequest, DateTimeOffset.UtcNow.AddMinutes(5)); + + context.ResponseInformation.State = passwordChangeRequest.Id; + context.ResponseInformation.ReplyMessage = "Please repeat new password: "; + + return ChallengeStatus.InProcess; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs new file mode 100644 index 00000000..df3c446c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs @@ -0,0 +1,14 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; + +public interface IChallengeProcessor +{ + //TODO DO NOT change context. Must return some response with required data + ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context); + bool HasChallengeContext(ChallengeIdentifier identifier); + Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context); + public ChallengeType ChallengeType { get; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs new file mode 100644 index 00000000..fb4611ba --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs @@ -0,0 +1,10 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; + +public interface IChallengeProcessorProvider +{ + IChallengeProcessor? GetChallengeProcessorByIdentifier(ChallengeIdentifier identifier); + IChallengeProcessor? GetChallengeProcessorByType(ChallengeType type); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeIdentifier.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeIdentifier.cs similarity index 90% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeIdentifier.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeIdentifier.cs index 890a963b..6a8d6cf4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeIdentifier.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeIdentifier.cs @@ -1,6 +1,6 @@ using Multifactor.Core.Ldap; -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; public class ChallengeIdentifier : ValueObject { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs new file mode 100644 index 00000000..099d87bf --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; + +public enum ChallengeStatus +{ + Reject = 0, + InProcess, + Accept +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs new file mode 100644 index 00000000..1a9d33d8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; + +public enum ChallengeType +{ + None = 0, + SecondFactor, + PasswordChange +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs new file mode 100644 index 00000000..84a1396e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; + +public class PasswordChangeCache +{ + public string Id { get; private set; } = Guid.NewGuid().ToString(); + public string Domain { get; set; } + public string CurrentPasswordEncryptedData { get; set; } + public string NewPasswordEncryptedData { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PersonalData.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PersonalData.cs similarity index 75% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PersonalData.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PersonalData.cs index ab697298..9842632a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PersonalData.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PersonalData.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; public class PersonalData { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs new file mode 100644 index 00000000..bb6b4de5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs @@ -0,0 +1,284 @@ +using System.Text; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; + +public class SecondFactorChallengeProcessor : IChallengeProcessor +{ + private readonly IMemoryCache _memoryCache; + private readonly MultifactorApiService _apiService; + private readonly ILdapAdapter _ldapAdapter; + private readonly ILogger _logger; + private readonly TimeSpan _defaultCacheDuration = TimeSpan.FromMinutes(10); + private readonly MemoryCacheEntryOptions _cacheOptions; + + public ChallengeType ChallengeType => ChallengeType.SecondFactor; + + public SecondFactorChallengeProcessor( + MultifactorApiService apiAdapter, + ILdapAdapter ldapAdapter, + ILogger logger, + IMemoryCache memoryCache) + { + _apiService = apiAdapter; + _ldapAdapter = ldapAdapter; + _logger = logger; + _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); + + var cacheDuration = _defaultCacheDuration; + _cacheOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = cacheDuration, + SlidingExpiration = null, //security sensitive + Priority = CacheItemPriority.Normal, + Size = 1, + PostEvictionCallbacks = + { + new PostEvictionCallbackRegistration + { + EvictionCallback = (key, value, reason, state) => + { + if (value is RadiusPipelineContext context) + { + logger.LogDebug("Challenge context evicted: {Key}, reason: {Reason}, message id={Id}", + key, reason, context.RequestPacket.Identifier); + } + } + } + } + }; + } + + public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + ArgumentException.ThrowIfNullOrWhiteSpace(context.ResponseInformation.State); + + var id = new ChallengeIdentifier(context.ClientConfiguration.Name, context.ResponseInformation.State); + var key = CreateCacheKey(id); + + try + { + _memoryCache.Set(key, context, _cacheOptions); + _logger.LogInformation("Challenge {State:l} was added for message id={id} (cached until {expiration})", + id.RequestId, + context.RequestPacket.Identifier, + DateTime.UtcNow.Add(_cacheOptions.AbsoluteExpirationRelativeToNow!.Value)); + + return id; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to cache request id={id} for the '{cfg:l}' configuration", + context.RequestPacket.Identifier, + context.ClientConfiguration.Name); + return ChallengeIdentifier.Empty; + } + } + + public bool HasChallengeContext(ChallengeIdentifier identifier) + { + var key = CreateCacheKey(identifier); + return _memoryCache.TryGetValue(key, out _); + } + + public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) + { + _logger.LogInformation("Processing challenge {State:l} for message id={id} from {host:l}:{port}", + identifier.RequestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + + var userName = context.RequestPacket.UserName; + if (string.IsNullOrWhiteSpace(userName)) + return ProcessEmptyName(context, identifier.RequestId); + + var challengeStatus = ProcessAuthenticationType(context, context.Passphrase, identifier.RequestId, out var userAnswer); + if (challengeStatus == ChallengeStatus.Reject) + return challengeStatus; + + var challengeContext = GetChallengeContext(identifier) ?? throw new InvalidOperationException($"Challenge context with identifier '{identifier}' was not found"); + var shouldCacheResponse = ShouldCacheResponse(context); + var response = await _apiService.SendChallengeAsync(challengeContext, shouldCacheResponse, identifier.RequestId, userAnswer!); + + return ProcessResponse(context, challengeContext, response, identifier); + } + + private RadiusPipelineContext? GetChallengeContext(ChallengeIdentifier identifier) + { + var key = CreateCacheKey(identifier); + + if (_memoryCache.TryGetValue(key, out RadiusPipelineContext? context)) + { + _logger.LogDebug("Retrieved challenge context for {State:l}", identifier.RequestId); + return context; + } + + _logger.LogError("Unable to get cached request with state={identifier:l}", identifier); + return null; + } + + private void RemoveChallengeContext(ChallengeIdentifier identifier) + { + var key = CreateCacheKey(identifier); + _memoryCache.Remove(key); + _logger.LogDebug("Removed challenge context for {State:l}", identifier.RequestId); + } + + private static string CreateCacheKey(ChallengeIdentifier identifier) + { + return $"Challenge:{identifier.ToString()}"; + } + + private ChallengeStatus ProcessAuthenticationType(RadiusPipelineContext context, UserPassphrase passphrase, string requestId, out string? userAnswer) + { + userAnswer = string.Empty; + switch (context.RequestPacket.AuthenticationType) + { + case AuthenticationType.PAP: + //user-password attribute holds second request challenge from user + userAnswer = passphrase.Raw; + + if (string.IsNullOrWhiteSpace(userAnswer)) + { + _logger.LogWarning( + "Can't find User-Password with user response in message id={id} from {host:l}:{port}", + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.ResponseInformation.State = requestId; + + return ChallengeStatus.Reject; + } + + return ChallengeStatus.InProcess; + case AuthenticationType.MSCHAP2: + var msChapResponse = context.RequestPacket.GetAttribute("MS-CHAP2-Response"); + + if (msChapResponse == null) + { + _logger.LogWarning( + "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Can't find MS-CHAP2-Response", + requestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.ResponseInformation.State = requestId; + + return ChallengeStatus.Reject; + } + + //forti behaviour + var otpData = msChapResponse.Skip(2).Take(6).ToArray(); + userAnswer = Encoding.ASCII.GetString(otpData); + return ChallengeStatus.InProcess; + default: + _logger.LogWarning( + "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Unsupported authentication type '{Auth}'", + requestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, + context.RequestPacket.AuthenticationType); + + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.ResponseInformation.State = requestId; + + return ChallengeStatus.Reject; + } + } + + private ChallengeStatus ProcessResponse(RadiusPipelineContext context, RadiusPipelineContext challengeContext, SecondFactorResponse response, ChallengeIdentifier identifier) + { + context.ResponseInformation.ReplyMessage = response.ReplyMessage; + switch (response.Code) + { + case AuthenticationStatus.Accept: + context.ResponsePacket = challengeContext.ResponsePacket; + context.LdapProfile = challengeContext.LdapProfile; + context.FirstFactorStatus = challengeContext.FirstFactorStatus; + context.SecondFactorStatus = AuthenticationStatus.Accept; + context.Passphrase = challengeContext.Passphrase; + + RemoveChallengeContext(identifier); + + _logger.LogDebug( + "Challenge {State:l} was processed for message id={id} from {host:l}:{port} with result '{Result}'", + identifier.RequestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, + response.Code); + + return ChallengeStatus.Accept; + + case AuthenticationStatus.Reject: + RemoveChallengeContext(identifier); + _logger.LogDebug( + "Challenge {State:l} was processed for message id={id} from {host:l}:{port} with result '{Result}'", + identifier.RequestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, + response.Code); + + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.ResponseInformation.State = identifier.RequestId; + + return ChallengeStatus.Reject; + + default: + context.ResponseInformation.State = identifier.RequestId; + + return ChallengeStatus.InProcess; + } + } + + private ChallengeStatus ProcessEmptyName(RadiusPipelineContext context, string requestId) + { + _logger.LogWarning( + "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Can't find User-Name", + requestId, + context.RequestPacket.Identifier, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.ResponseInformation.State = requestId; + + return ChallengeStatus.Reject; + } + + private bool ShouldCacheResponse(RadiusPipelineContext context) + { + if (context.LdapConfiguration is null || context.LdapConfiguration.AuthenticationCacheGroups.Count == 0) + return true; + + var cacheGroups = context.LdapConfiguration.AuthenticationCacheGroups; + if (context.LdapProfile.MemberOf.Intersect(cacheGroups).Any()) return true; + var isMember = _ldapAdapter.IsMemberOf(MembershipRequest.FromContext(context, cacheGroups)); + var groupsStr = string.Join(',', cacheGroups); + var username = context.RequestPacket.UserName; + _logger.LogDebug( + !isMember + ? "User '{userName}' is not a member of any authentication cache groups: ({groups})" + : "User '{userName}' is a member of authentication cache groups: ({groups})", username, groupsStr); + + return isMember; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs new file mode 100644 index 00000000..68a3fa47 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs @@ -0,0 +1,21 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +public class ActiveDirectoryFormatter : ILdapBindNameFormatter +{ + public LdapImplementation LdapImplementation => LdapImplementation.ActiveDirectory; + + public string FormatName(string userName, ILdapProfile ldapProfile) + { + var identity = new UserIdentity(userName); + + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) + return userName; + + return ldapProfile.Dn.StringRepresentation; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs new file mode 100644 index 00000000..61749df0 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs @@ -0,0 +1,21 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +public class FreeIpaFormatter : ILdapBindNameFormatter +{ + public LdapImplementation LdapImplementation => LdapImplementation.FreeIPA; + + public string FormatName(string userName, ILdapProfile ldapProfile) + { + var identity = new UserIdentity(userName); + + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) + return userName; + + return ldapProfile.Dn.StringRepresentation; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs similarity index 54% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs index 9f76dbe3..9a55a5b4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs @@ -1,7 +1,7 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public interface ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs similarity index 65% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs index 57edacf8..d517b5c2 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs @@ -1,6 +1,6 @@ using Multifactor.Core.Ldap.Schema; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public interface ILdapBindNameFormatterProvider { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs similarity index 83% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs index d59f35f3..af4c4d58 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs @@ -1,10 +1,10 @@ using Multifactor.Core.Ldap.Schema; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class LdapBindNameFormatterProvider : ILdapBindNameFormatterProvider { - private readonly List _formatters = new(); + private readonly List _formatters = []; public LdapBindNameFormatterProvider(IEnumerable formatters) { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs new file mode 100644 index 00000000..9808f39c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs @@ -0,0 +1,21 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +public class MultiDirectoryFormatter : ILdapBindNameFormatter +{ + public LdapImplementation LdapImplementation => LdapImplementation.MultiDirectory; + + public string FormatName(string userName, ILdapProfile ldapProfile) + { + var identity = new UserIdentity(userName); + + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) + return userName; + + return ldapProfile.Dn.StringRepresentation; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs new file mode 100644 index 00000000..cf5d100e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs @@ -0,0 +1,21 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +public class OpenLdapFormatter: ILdapBindNameFormatter +{ + public LdapImplementation LdapImplementation => LdapImplementation.OpenLDAP; + + public string FormatName(string userName, ILdapProfile ldapProfile) + { + var identity = new UserIdentity(userName); + + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) + return userName; + + return ldapProfile.Dn.StringRepresentation; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs new file mode 100644 index 00000000..6ca23792 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs @@ -0,0 +1,21 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +public class SambaFormatter : ILdapBindNameFormatter +{ + public LdapImplementation LdapImplementation => LdapImplementation.Samba; + + public string FormatName(string userName, ILdapProfile ldapProfile) + { + var identity = new UserIdentity(userName); + + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) + return userName; + + return ldapProfile.Dn.StringRepresentation; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/FirstFactorProcessorProvider.cs similarity index 76% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/FirstFactorProcessorProvider.cs index eb87cc2f..f44cb3a7 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/FirstFactorProcessorProvider.cs @@ -1,7 +1,6 @@ -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class FirstFactorProcessorProvider : IFirstFactorProcessorProvider { @@ -9,7 +8,7 @@ public class FirstFactorProcessorProvider : IFirstFactorProcessorProvider public FirstFactorProcessorProvider(IEnumerable processors) { - Throw.IfNull(processors, nameof(processors)); + ArgumentNullException.ThrowIfNull(processors); _firstFactorProcessors = processors; } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs new file mode 100644 index 00000000..be78de00 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs @@ -0,0 +1,11 @@ +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; + +public interface IFirstFactorProcessor +{ + // TODO remove 'context' from signature. Create ff request and response + Task ProcessFirstFactor(RadiusPipelineContext context); + AuthenticationSource AuthenticationSource { get; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessorProvider.cs new file mode 100644 index 00000000..4fee913f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessorProvider.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; + +public interface IFirstFactorProcessorProvider +{ + IFirstFactorProcessor GetProcessor(AuthenticationSource authSource); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs new file mode 100644 index 00000000..d5ef49e9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs @@ -0,0 +1,178 @@ +using System.DirectoryServices.Protocols; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; + +public class LdapFirstFactorProcessor : IFirstFactorProcessor +{ + private readonly ILdapBindNameFormatterProvider _ldapBindNameFormatterProvider; + private readonly ILogger _logger; + private readonly ILdapAdapter _ldapAdapter; + + public AuthenticationSource AuthenticationSource => AuthenticationSource.Ldap; + + public LdapFirstFactorProcessor(ILdapBindNameFormatterProvider ldapBindNameFormatterProvider, ILogger logger, ILdapAdapter ldapAdapter) + {; + _logger = logger; + _ldapAdapter = ldapAdapter; + _ldapBindNameFormatterProvider = ldapBindNameFormatterProvider; + } + + public Task ProcessFirstFactor(RadiusPipelineContext context) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + + var radiusPacket = context.RequestPacket; + + if (context.LdapConfiguration is null) + throw new InvalidOperationException("No Ldap servers configured."); + + if (string.IsNullOrWhiteSpace(radiusPacket.UserName)) + { + _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); + Reject(context); + return Task.CompletedTask; + } + + var transformedName = radiusPacket.UserName; + + var passphrase = context.Passphrase; + if (string.IsNullOrWhiteSpace(passphrase.Raw)) + { + _logger.LogWarning("No User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); + Reject(context); + return Task.CompletedTask; + } + + if (string.IsNullOrWhiteSpace(passphrase.Password)) + { + _logger.LogWarning("Can't parse User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); + Reject(context); + return Task.CompletedTask; + } + + var isValid = ValidateUserCredentials(context, transformedName, passphrase.Password); + if (!isValid) + { + Reject(context); + return Task.CompletedTask; + } + + _logger.LogInformation("User '{user:l}' credential and status verified successfully at {endpoint:l}", transformedName, context.LdapConfiguration.ConnectionString); + Accept(context); + return Task.CompletedTask; + } + + private bool ValidateUserCredentials( + RadiusPipelineContext context, + string login, + string password) + { + var serverConfig = context.LdapConfiguration; + if (serverConfig is null) + throw new InvalidOperationException("No Ldap servers configured."); + + var bindName = string.Empty; + + try + { + var ldapImpl = context.LdapSchema!.LdapServerImplementation; + var formatter = _ldapBindNameFormatterProvider.GetLdapBindNameFormatter(ldapImpl); + if (formatter is null) + _logger.LogWarning("No LDAP bind name formatter configured for '{implementation}' implementation.", ldapImpl); + + var formatted = string.Empty; + if (context.LdapProfile is not null) + formatted = formatter?.FormatName(login, context.LdapProfile); + + bindName = string.IsNullOrWhiteSpace(formatted) ? login : formatted; + + _logger.LogDebug("Use '{name}' for LDAP bind.", bindName); + var request = new LdapConnectionData + { + ConnectionString = serverConfig.ConnectionString, + UserName = bindName, + Password = password, + BindTimeoutInSeconds = serverConfig.BindTimeoutSeconds + }; + + return _ldapAdapter.CheckConnection(request); + } + catch (Exception ex) + { + if (ex is not LdapException ldapException) + { + _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", bindName, serverConfig.ConnectionString); + return false; + } + if(CheckLdapException(ldapException, out var reasonText)) + context.MustChangePasswordDomain = context.LdapConfiguration.ConnectionString; + + _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", bindName, serverConfig.ConnectionString, reasonText); + } + + return false; + } + + private static void Reject(RadiusPipelineContext context) + { + context.FirstFactorStatus = AuthenticationStatus.Reject; + } + + private static void Accept(RadiusPipelineContext context) + { + context.FirstFactorStatus = AuthenticationStatus.Accept; + } + + + private static bool CheckLdapException(LdapException exception, out string reasonText) + { + if (string.IsNullOrWhiteSpace(exception.ServerErrorMessage)) + { + reasonText = "UnknownError"; + return false; + } + + var pattern = @"data ([0-9a-e]{3})"; + var match = Regex.Match(exception.ServerErrorMessage, pattern); + + if (!match.Success || match.Groups.Count != 2) + { + reasonText = "UnknownError"; + return false; + } + + var data = match.Groups[1].Value; + switch (data) + { + case "525": reasonText = "UserNotFound"; + break; + case "52e": reasonText = "InvalidCredentials"; + break; + case "530": reasonText = "NotPermittedToLogonAtThisTime"; + break; + case "531": reasonText = "NotPermittedToLogonAtThisWorkstation"; + break; + case "532": reasonText = "PasswordExpired"; + return true; + case "533": reasonText = "AccountDisabled"; + break; + case "701": reasonText = "AccountExpired"; + break; + case "773": reasonText = "UserMustChangePassword"; + return true; + case "775": reasonText = "UserAccountLocked"; + break; + default: reasonText = "UnknownError"; + break; + } + return false; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/NoneFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/NoneFirstFactorProcessor.cs similarity index 50% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/NoneFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/NoneFirstFactorProcessor.cs index 925a6836..2b18a80c 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/NoneFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/NoneFirstFactorProcessor.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class NoneFirstFactorProcessor : IFirstFactorProcessor { @@ -13,10 +14,10 @@ public NoneFirstFactorProcessor(ILogger logger) { _logger = logger; } - public Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) + public Task ProcessFirstFactor(RadiusPipelineContext context) { - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - _logger.LogInformation("Bypass first factor for user '{user:l}' due to '{ff:l}' variant of first factor.", context.RequestPacket.UserName, context.FirstFactorAuthenticationSource.ToString()); + context.FirstFactorStatus = AuthenticationStatus.Accept; + _logger.LogInformation("Bypass first factor for user '{user:l}' due to '{ff:l}' variant of first factor.", context.RequestPacket.UserName, context.ClientConfiguration.FirstFactorAuthenticationSource.ToString()); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/RadiusFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs similarity index 65% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/RadiusFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs index 2f02fa81..33c2559d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/RadiusFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs @@ -1,12 +1,13 @@ using System.Net; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Radius; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class RadiusFirstFactorProcessor : IFirstFactorProcessor { @@ -23,33 +24,33 @@ public RadiusFirstFactorProcessor(IRadiusPacketService radiusPacketService, IRad _logger = logger; } - public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) + public async Task ProcessFirstFactor(RadiusPipelineContext context) { - Throw.IfNull(context, nameof(context)); + ArgumentNullException.ThrowIfNull(context, nameof(context)); var requestPacket = context.RequestPacket; - Throw.IfNull(requestPacket, nameof(requestPacket)); + ArgumentNullException.ThrowIfNull(requestPacket, nameof(requestPacket)); if (string.IsNullOrWhiteSpace(requestPacket.UserName)) { - _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", context.RequestPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - context.AuthenticationState.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); + _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", context.RequestPacket.Identifier, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); + context.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); return; } try { - var transformedName = UserNameTransformation.Transform(requestPacket.UserName, context.UserNameTransformRules.BeforeFirstFactor); + var transformedName = requestPacket.UserName; var authPacket = PreparePacket(requestPacket, transformedName, context.Passphrase); - var authBytes = _radiusPacketService.GetBytes(authPacket, context.RadiusSharedSecret); + var authBytes = _radiusPacketService.SerializePacket(authPacket, new SharedSecret(context.ClientConfiguration.RadiusSharedSecret)); byte[]? response = null; IPEndPoint? endPoint = null; - using var client = _radiusClientFactory.CreateRadiusClient(context.ServiceClientEndpoint); - foreach (var npsEndPoint in context.NpsServerEndpoints) + using var client = _radiusClientFactory.CreateRadiusClient(context.ClientConfiguration.AdapterClientEndpoint); + foreach (var npsEndPoint in context.ClientConfiguration.NpsServerEndpoints) { - response = await SendRequestToNpsServer(client, npsEndPoint, authPacket.Identifier, requestPacket.Identifier, authBytes, context.NpsServerTimeout); + response = await SendRequestToNpsServer(client, npsEndPoint, authPacket.Identifier, requestPacket.Identifier, authBytes, context.ClientConfiguration.NpsServerTimeout); if (response is not null) { endPoint = npsEndPoint; @@ -61,11 +62,11 @@ public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) if (response is null) { - context.AuthenticationState.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); + context.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); return; } - var responsePacket = _radiusPacketService.Parse(response, context.RadiusSharedSecret, authPacket.Authenticator); + var responsePacket = _radiusPacketService.ParsePacket(response, new SharedSecret(context.ClientConfiguration.RadiusSharedSecret), authPacket.Authenticator); _logger.LogDebug("Received {code:l} message with id={id} from Remote Radius Server", authPacket.Code.ToString(), authPacket.Identifier); if (responsePacket.Code == PacketCode.AccessAccept) @@ -75,7 +76,7 @@ public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) } context.ResponsePacket = responsePacket; - context.AuthenticationState.FirstFactorStatus = GetAuthState(responsePacket.Code); + context.FirstFactorStatus = GetAuthState(responsePacket.Code); return; } catch (Exception ex) @@ -83,7 +84,7 @@ public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) _logger.LogError(ex, "Radius authentication error"); } - context.AuthenticationState.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); + context.FirstFactorStatus = GetAuthState(PacketCode.AccessReject); } private async Task SendRequestToNpsServer(IRadiusClient client, IPEndPoint npsServerEndpoint, byte authIdentifier, byte requestIdentifier, byte[] payload, TimeSpan timeout) @@ -92,7 +93,7 @@ public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) return await client.SendPacketAsync(authIdentifier, payload, npsServerEndpoint, timeout); } - private IRadiusPacket PreparePacket(IRadiusPacket radiusPacket, string userName, UserPassphrase passphrase) + private static RadiusPacket PreparePacket(RadiusPacket radiusPacket, string userName, UserPassphrase passphrase) { var authPacket = new RadiusPacket(new RadiusPacketHeader(radiusPacket.Code, radiusPacket.Identifier, radiusPacket.Authenticator)); @@ -103,7 +104,6 @@ private IRadiusPacket PreparePacket(IRadiusPacket radiusPacket, string userName, } authPacket.RemoveAttribute("Proxy-State"); - // authPacket.RemoveAttribute("State"); MF NPS does not send a response with State, but it should be authPacket.ReplaceAttribute("User-Name", userName); if (!string.IsNullOrWhiteSpace(passphrase.Password)) @@ -112,7 +112,7 @@ private IRadiusPacket PreparePacket(IRadiusPacket radiusPacket, string userName, return authPacket; } - private AuthenticationStatus GetAuthState(PacketCode responseCode) => responseCode switch + private static AuthenticationStatus GetAuthState(PacketCode responseCode) => responseCode switch { PacketCode.AccessAccept => AuthenticationStatus.Accept, _ => AuthenticationStatus.Reject diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs new file mode 100644 index 00000000..feb98bbe --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; + +public interface IPipelineProvider +{ + public IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs new file mode 100644 index 00000000..72b1ecd0 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; + +public interface IRadiusPipeline +{ + Task ExecuteAsync(RadiusPipelineContext context); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs new file mode 100644 index 00000000..9af4918f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; + +public interface IRadiusPipelineFactory +{ + IRadiusPipeline CreatePipeline(IClientConfiguration clientConfig); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/AuthenticationStatus.cs similarity index 50% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/AuthenticationStatus.cs index f0235e4c..dc9be44f 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/AuthenticationStatus.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; public enum AuthenticationStatus { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentityFormat.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/UserIdentityFormat.cs similarity index 68% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentityFormat.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/UserIdentityFormat.cs index 846dacef..3639b387 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentityFormat.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/UserIdentityFormat.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Identity +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum { public enum UserIdentityFormat { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs new file mode 100644 index 00000000..978c382d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs @@ -0,0 +1,41 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +public class RadiusPipelineContext +{ + public RadiusPacket RequestPacket { get; } + public IClientConfiguration ClientConfiguration { get; } + public ILdapServerConfiguration? LdapConfiguration { get; } + public UserPassphrase? Passphrase { get; set; } + public ILdapSchema? LdapSchema { get; set; } + public ILdapProfile? LdapProfile { get; set; } + public string MustChangePasswordDomain { get; set; } + public HashSet UserGroups { get; set; } = []; + + public RadiusPacket? ResponsePacket { get; set; } + public ResponseInformation ResponseInformation { get; set; } = new(); + public AuthenticationStatus FirstFactorStatus { get; set; } + public AuthenticationStatus SecondFactorStatus { get; set; } + + public bool IsTerminated { get; private set; } + public bool ShouldSkipResponse { get; private set; } + public bool IsDomainAccount => RequestPacket.AccountType == AccountType.Domain; + public void Terminate() => IsTerminated = true; + public void SkipResponse() => ShouldSkipResponse = true; + + public RadiusPipelineContext( + RadiusPacket requestPacket, + IClientConfiguration clientConfiguration, + ILdapServerConfiguration? ldapServerConfig = null) + { + RequestPacket = requestPacket; + ClientConfiguration = clientConfiguration; + LdapConfiguration = ldapServerConfig; + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/ResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/ResponseInformation.cs new file mode 100644 index 00000000..271abc35 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/ResponseInformation.cs @@ -0,0 +1,7 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +public class ResponseInformation +{ + public string? ReplyMessage { get; set; } + public string? State { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs new file mode 100644 index 00000000..0d2a3ac3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs @@ -0,0 +1,50 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +public class UserIdentity +{ + public string Identity { get; init; } + public UserIdentityFormat Format { get; init; } + + public UserIdentity(string identity) + { + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); + Identity = identity; + Format = GetIdentityTypeByIdentity(identity); + } + + public UserIdentity(string identity, UserIdentityFormat format) + { + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); + Identity = identity; + Format = format; + } + + private static UserIdentityFormat GetIdentityTypeByIdentity(string identity) + { + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); + + var id = identity.ToLower(); + + if (id.Contains('\\')) + return UserIdentityFormat.NetBiosName; + + if (id.Contains('=')) + return UserIdentityFormat.DistinguishedName; + + if (id.Contains('@')) + return UserIdentityFormat.UserPrincipalName; + + return UserIdentityFormat.SamAccountName; + } + + public string GetUpnSuffix() + { + if (Format != UserIdentityFormat.UserPrincipalName) + return string.Empty; + + var suffix = Identity.Split('@', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Last(); + return suffix; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs similarity index 60% rename from src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs index 4218fd84..0af47ac1 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs @@ -1,22 +1,25 @@ -using System.Text.RegularExpressions; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md -namespace Multifactor.Radius.Adapter.v2.Core; +using System.Text.RegularExpressions; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; -public class UserPassphrase +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models +{ + public class UserPassphrase { - private static readonly string[] ProviderCodes = { "t", "m", "s", "c" }; + private static readonly string[] _providerCodes = { "t", "m", "s", "c" }; /// /// User-Password attribute raw value. /// - public string? Raw { get; } + public string Raw { get; } /// /// User password. /// - public string? Password { get; } + public string Password { get; } /// /// 6 digits. @@ -31,14 +34,14 @@ public class UserPassphrase /// c: PhoneCall
/// Can be passed to the User-Password attribute in case of None first-factor-authentication-source or if challenge is executed. ///
- public string? ProviderCode { get; } + public string ProviderCode { get; } /// /// User-Password packet attribute is empty. /// public bool IsEmpty => Password == null && Otp == null && ProviderCode == null; - private UserPassphrase(string? raw, string? password, string? otp, string? providerCode) + private UserPassphrase(string raw, string password, string otp, string providerCode) { Raw = raw; Password = password; @@ -46,34 +49,40 @@ private UserPassphrase(string? raw, string? password, string? otp, string? provi ProviderCode = providerCode; } - public static UserPassphrase Parse(string? rawPwd, PreAuthModeDescriptor preAuthnMode) + public static UserPassphrase Parse(string rawPwd, PreAuthMode? preAuthnMode) { - Throw.IfNull(preAuthnMode, nameof(preAuthnMode)); - - var hasOtp = TryGetOtpCode(rawPwd, preAuthnMode, out var otp); + var hasOtp = TryGetOtpCode(rawPwd, out var otp); if (!hasOtp) + { otp = null; + } var pwd = GetPassword(rawPwd, preAuthnMode, hasOtp); - if (string.IsNullOrWhiteSpace(pwd)) + if (string.IsNullOrEmpty(pwd)) + { pwd = null; + } - var provCode = ProviderCodes.FirstOrDefault(x => x == pwd?.ToLower()); + var provCode = _providerCodes.FirstOrDefault(x => x == pwd?.ToLower()); return new UserPassphrase(rawPwd, pwd, otp, provCode); } - private static string GetPassword(string? rawPwd, PreAuthModeDescriptor preAuthnMode, bool hasOtp) + private static string GetPassword(string rawPwd, PreAuthMode? preAuthnMode, bool hasOtp) { var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; - switch (preAuthnMode.Mode) + switch (preAuthnMode) { case PreAuthMode.Otp: - var length = preAuthnMode.Settings.OtpCodeLength; + var length = 6; if (passwordAndOtp.Length < length) + { return passwordAndOtp; + } if (!hasOtp) + { return passwordAndOtp; + } var sub = passwordAndOtp[..^length]; return sub; @@ -84,10 +93,10 @@ private static string GetPassword(string? rawPwd, PreAuthModeDescriptor preAuthn } } - private static bool TryGetOtpCode(string? rawPwd, PreAuthModeDescriptor preAuthnMode, out string? code) + private static bool TryGetOtpCode(string rawPwd, out string code) { var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; - var length = preAuthnMode.Settings.OtpCodeLength; + var length = 10; if (passwordAndOtp.Length < length) { code = null; @@ -95,7 +104,8 @@ private static bool TryGetOtpCode(string? rawPwd, PreAuthModeDescriptor preAuthn } code = passwordAndOtp[^length..]; - if (!Regex.IsMatch(code, preAuthnMode.Settings.OtpCodeRegex)) + var otpCodeRegex = $"^[0-9]{{{length}}}$"; + if (!Regex.IsMatch(code, otpCodeRegex)) { code = null; return false; @@ -103,4 +113,5 @@ private static bool TryGetOtpCode(string? rawPwd, PreAuthModeDescriptor preAuthn return true; } - } \ No newline at end of file + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs new file mode 100644 index 00000000..4b946e1c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs @@ -0,0 +1,29 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; + +public class RadiusPipeline : IRadiusPipeline +{ + private readonly List _steps; + + public RadiusPipeline(List steps) + { + _steps = steps ?? throw new ArgumentNullException(nameof(steps)); + } + + public async Task ExecuteAsync(RadiusPipelineContext context) + { + ArgumentNullException.ThrowIfNull(context); + foreach (var step in _steps) + { + await step.ExecuteAsync(context); + + if (context.IsTerminated) + { + break; + } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs new file mode 100644 index 00000000..0aac8a27 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs @@ -0,0 +1,96 @@ +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; + +public class RadiusPipelineFactory : IRadiusPipelineFactory +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public RadiusPipelineFactory( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public IRadiusPipeline CreatePipeline(IClientConfiguration clientConfig) + { + var steps = CreatePipelineSteps(clientConfig); + LogProviderCreated(clientConfig.Name, steps); + return new RadiusPipeline(steps); + } + + private List CreatePipelineSteps(IClientConfiguration clientConfig) + { + var withLdap = clientConfig.LdapServers?.Count > 0; + var steps = new List + { + CreateStep(), + CreateStep(), + CreateStep() + }; + + if (withLdap) + { + steps.Add(CreateStep()); + steps.Add(CreateStep()); + steps.Add(CreateStep()); + steps.Add(CreateStep()); + } + + steps.Add(CreateStep()); + + if (clientConfig.PreAuthenticationMethod != PreAuthMode.None) + { + steps.Add(CreateStep()); + steps.Add(CreateStep()); + steps.Add(CreateStep()); + steps.Add(CreateStep()); + } + else + { + steps.Add(CreateStep()); + steps.Add(CreateStep()); + } + + if (withLdap && ShouldLoadUserGroups(clientConfig)) + { + steps.Add(CreateStep()); + } + + return steps; + } + + private IRadiusPipelineStep CreateStep() where TStep : IRadiusPipelineStep + { + return _serviceProvider.GetRequiredService(); + } + + private void LogProviderCreated(string configName, List steps) + { + var builder = new StringBuilder(); + builder.AppendLine($"Configuration: {configName}"); + builder.AppendLine("Steps:"); + for (var i = 0; i < steps.Count; i++) + { + builder.AppendLine($"{i+1}. {steps[i].GetType().Name}"); + } + + _logger.LogDebug(builder.ToString()); + } + + private static bool ShouldLoadUserGroups(IClientConfiguration config) => config + .ReplyAttributes != null && config + .ReplyAttributes + .Values + .SelectMany(x => x) + .Any(x => x.IsMemberOf || x.UserGroupCondition.Count > 0); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs new file mode 100644 index 00000000..c1e90ba2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; + +public class RadiusPipelineProvider : IPipelineProvider +{ + private readonly IRadiusPipelineFactory _pipelineFactory; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _pipelineCache = new(); + + public RadiusPipelineProvider( + IRadiusPipelineFactory pipelineFactory, + ILogger logger) + { + _pipelineFactory = pipelineFactory; + _logger = logger; + } + + public IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration) + { + var clientName = clientConfiguration.Name; + return _pipelineCache.GetOrAdd(clientName, name => + { + _logger.LogDebug("Creating new pipeline for client '{Client}'", name); + return _pipelineFactory.CreatePipeline(clientConfiguration); + }); + } + +} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessChallengeStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs similarity index 69% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessChallengeStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs index e823019d..b66de84a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessChallengeStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class AccessChallengeStep : IRadiusPipelineStep { @@ -14,13 +16,13 @@ public AccessChallengeStep(IChallengeProcessorProvider challengeProcessorProvide _logger = logger; } - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public async Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(AccessChallengeStep)); if (string.IsNullOrWhiteSpace(context.RequestPacket.State)) return; - var identifier = new ChallengeIdentifier(context.ClientConfigurationName, context.RequestPacket.State); + var identifier = new ChallengeIdentifier(context.ClientConfiguration.Name, context.RequestPacket.State); var processor = _challengeProcessorProvider.GetChallengeProcessorByIdentifier(identifier); if (processor is null) @@ -35,7 +37,7 @@ public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) case ChallengeStatus.Reject: case ChallengeStatus.InProcess: - context.ExecutionState.Terminate(); + context.Terminate(); return; default: diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs new file mode 100644 index 00000000..5e217204 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public class AccessGroupsCheckingStep : IRadiusPipelineStep +{ + private readonly ILdapAdapter _ldapAdapter; + private readonly ILogger _logger; + + public AccessGroupsCheckingStep( + ILdapAdapter ldapAdapter, + ILogger logger) + { + _ldapAdapter = ldapAdapter; + _logger = logger; + } + + public Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("'{name}' started", nameof(AccessGroupsCheckingStep)); + ArgumentNullException.ThrowIfNull(context, nameof(context)); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration, nameof(context.LdapConfiguration)); + ArgumentNullException.ThrowIfNull(context.LdapSchema, nameof(context.LdapSchema)); + + if (ShouldSkipStep(context)) + return Task.CompletedTask; + + ArgumentNullException.ThrowIfNull(context.LdapProfile, nameof(context.LdapProfile)); + var accessGroup = context.LdapConfiguration.AccessGroups; + var request = MembershipRequest.FromContext(context, accessGroup); + var isMember = context.LdapProfile.MemberOf.Intersect(accessGroup).Any() || _ldapAdapter.IsMemberOf(request); + + return isMember ? Task.CompletedTask : TerminatePipeline(context); + } + + private Task TerminatePipeline(RadiusPipelineContext context) + { + _logger.LogWarning("User '{user}' is not member of any access group of the '{connectionString}'.", + context.LdapProfile!.Dn, context.LdapConfiguration!.ConnectionString); + context.FirstFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.Terminate(); + return Task.CompletedTask; + } + + private bool ShouldSkipStep(RadiusPipelineContext context) + { + return NoAccessGroups(context) || UnsupportedAccountType(context); + } + + private bool NoAccessGroups(RadiusPipelineContext config) + { + var noGroups = config.LdapConfiguration!.AccessGroups.Count == 0; + + if (!noGroups) + return false; + + _logger.LogDebug("No access groups were specified."); + return true; + } + + private bool UnsupportedAccountType(RadiusPipelineContext context) + { + if (context.IsDomainAccount) + return false; + + var packet = context.RequestPacket; + _logger.LogInformation( + "User '{user}' used '{accountType}' account to log in. Access groups checking is skipped.", + packet.UserName, + packet.AccountType); + + return true; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessRequestFilteringStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessRequestFilteringStep.cs new file mode 100644 index 00000000..177f9656 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessRequestFilteringStep.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public class AccessRequestFilteringStep : IRadiusPipelineStep +{ + private readonly ILogger _logger; + private const string StepName = nameof(AccessRequestFilteringStep); + + public AccessRequestFilteringStep(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("'{StepName}' started", StepName); + + if (context.RequestPacket.Code == PacketCode.AccessRequest) + { + return Task.CompletedTask; + } + + LogUnprocessablePacket(context); + context.Terminate(); + context.SkipResponse(); + + return Task.CompletedTask; + } + + private void LogUnprocessablePacket(RadiusPipelineContext context) + { + var client = context.RequestPacket.ProxyEndpoint?.Address + ?? context.RequestPacket.RemoteEndpoint?.Address; + var clientInfo = client?.ToString() ?? "unknown"; + + _logger.LogWarning( + "Unprocessable packet type: {PacketCode}, from {Client}", + context.RequestPacket.Code.ToString(), + clientInfo); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/FirstFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/FirstFactorStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs index 3b6104ef..9807e650 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/FirstFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs @@ -1,10 +1,11 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class FirstFactorStep : IRadiusPipelineStep { @@ -19,28 +20,28 @@ public FirstFactorStep(IFirstFactorProcessorProvider processorProvider, IChallen _logger = logger; } - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public async Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(FirstFactorStep)); ArgumentNullException.ThrowIfNull(context, nameof(context)); - if (context.AuthenticationState.FirstFactorStatus != AuthenticationStatus.Awaiting) + if (context.FirstFactorStatus != AuthenticationStatus.Awaiting) return; - var processor = _firstFactorProcessor.GetProcessor(context.FirstFactorAuthenticationSource); + var processor = _firstFactorProcessor.GetProcessor(context.ClientConfiguration.FirstFactorAuthenticationSource); await processor.ProcessFirstFactor(context); if (!string.IsNullOrWhiteSpace(context.MustChangePasswordDomain)) { var challengeProcessor = _challengeProcessorProviderProvider.GetChallengeProcessorByType(ChallengeType.PasswordChange); if (challengeProcessor is null) - throw new Exception($"Challenge processor for {context.FirstFactorAuthenticationSource} is not available"); + throw new Exception($"Challenge processor for {context.ClientConfiguration.FirstFactorAuthenticationSource} is not available"); challengeProcessor.AddChallengeContext(context); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.FirstFactorStatus = AuthenticationStatus.Awaiting; } - if (context.AuthenticationState.FirstFactorStatus != AuthenticationStatus.Accept) - context.ExecutionState.Terminate(); + if (context.FirstFactorStatus != AuthenticationStatus.Accept) + context.Terminate(); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IRadiusPipelineStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IRadiusPipelineStep.cs new file mode 100644 index 00000000..768a5b01 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IRadiusPipelineStep.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public interface IRadiusPipelineStep +{ + Task ExecuteAsync(RadiusPipelineContext context); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IpWhiteListStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs similarity index 65% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IpWhiteListStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs index f520aa9b..3299d932 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IpWhiteListStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs @@ -1,9 +1,9 @@ using System.Net; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class IpWhiteListStep : IRadiusPipelineStep { @@ -14,9 +14,9 @@ public IpWhiteListStep(ILogger logger) _logger = logger; } - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public Task ExecuteAsync(RadiusPipelineContext context) { - var ipWhiteList = context.IpWhiteList; + var ipWhiteList = context.ClientConfiguration.IpWhiteList; if (ipWhiteList.Count == 0) return Task.CompletedTask; @@ -24,7 +24,7 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) var clientIp = IPAddress.TryParse(callingStationId, out var callingStationIp) ? callingStationIp - : context.RemoteEndpoint.Address; + : context.RequestPacket.RemoteEndpoint.Address; var isIpInRange = ipWhiteList.Any(x => x.Contains(clientIp)); var rangesStr = string.Join(", ", ipWhiteList); @@ -36,9 +36,9 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) _logger.LogDebug("Client '{clientIp}' is not in the allowed IP range: ({ranges})", clientIp.ToString(), rangesStr); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ExecutionState.Terminate(); + context.FirstFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.Terminate(); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs new file mode 100644 index 00000000..2e521692 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public class LdapSchemaLoadingStep: IRadiusPipelineStep +{ + private readonly ILdapAdapter _ldapAdapter; + private readonly ICacheService _cache; + private readonly ILogger _logger; + private const int LdapSchemaCacheLifeTimeInHours = 1; + public LdapSchemaLoadingStep(ILdapAdapter ldapAdapter, ICacheService cache, ILogger logger) + { + _ldapAdapter = ldapAdapter; + _cache = cache; + _logger = logger; + } + + public Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("'{name}' started", nameof(LdapSchemaLoadingStep)); + ArgumentNullException.ThrowIfNull(context, nameof(context)); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration, nameof(context)); + + var schema = TryGetLdapSchema(context); + + if (schema is null) + { + _logger.LogWarning("Unable to load LDAP schema for '{domain}'", context.LdapConfiguration.ConnectionString); + throw new InvalidOperationException(); + } + + context.LdapSchema = schema; + return Task.CompletedTask; + } + + private ILdapSchema? TryGetLdapSchema(RadiusPipelineContext context) + { + var cacheKey = context.LdapConfiguration!.ConnectionString; + if (_cache.TryGetValue(cacheKey, out ILdapSchema? schema)) + { + _logger.LogDebug("Loaded LDAP schema for '{domain}' from cache.", cacheKey); + return schema; + } + + var request = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }; + schema = _ldapAdapter.LoadSchema(request); + + if (schema is null) + return schema; + + var expirationDate = DateTimeOffset.Now.AddHours(LdapSchemaCacheLifeTimeInHours); + _cache.Set(cacheKey, schema, expirationDate); + + _logger.LogDebug("LDAP schema for '{domain}' is saved in cache till '{expirationDate}'.", cacheKey, expirationDate.ToString()); + return schema; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs new file mode 100644 index 00000000..6aa4b604 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public class PreAuthCheckStep : IRadiusPipelineStep +{ + private readonly ILogger _logger; + private readonly ILdapAdapter _ldapAdapter; + + public PreAuthCheckStep(ILogger logger, ILdapAdapter ldapAdapter) + { + _logger = logger; + _ldapAdapter = ldapAdapter; + } + + public Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("'{name}' started", nameof(PreAuthCheckStep)); + + var isNeedOtp = SecondFaBypassGroupsDisableOrUserIsNotMemberOf(context); + switch (context.ClientConfiguration.PreAuthenticationMethod) + { + case PreAuthMode.Otp when isNeedOtp && context.Passphrase?.Otp == null: + context.SecondFactorStatus = AuthenticationStatus.Reject; + _logger.LogError("Pre-auth second factor was rejected: otp code is empty. User '{user:l}' from {host:l}:{port}", + context.RequestPacket.UserName, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + context.Terminate(); + return Task.CompletedTask; + + case PreAuthMode.None: + case PreAuthMode.Otp: + case PreAuthMode.Any: + _logger.LogDebug("Pre-auth check for '{user}' is completed.", context.RequestPacket.UserName); + return Task.CompletedTask; + + default: + throw new NotImplementedException($"Unknown pre-auth method: {context.ClientConfiguration.PreAuthenticationMethod}"); + } + } + + private bool SecondFaBypassGroupsDisableOrUserIsNotMemberOf(RadiusPipelineContext context) + { + var serverConfig = context.LdapConfiguration; + if (serverConfig is null) + return true; + + if (!serverConfig.SecondFaBypassGroups.Any()) + return true; + + var request = MembershipRequest.FromContext(context, serverConfig.SecondFaBypassGroups); + var isMemberOfBypassGroups = _ldapAdapter.IsMemberOf(request); + + return !isMemberOfBypassGroups; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthPostCheck.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs similarity index 65% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthPostCheck.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs index 900ab894..b4ba9974 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthPostCheck.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class PreAuthPostCheck : IRadiusPipelineStep { @@ -13,17 +13,17 @@ public PreAuthPostCheck(ILogger logger) _logger = logger; } - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(PreAuthPostCheck)); - if (context.AuthenticationState.SecondFactorStatus is AuthenticationStatus.Accept or AuthenticationStatus.Bypass) + if (context.SecondFactorStatus is AuthenticationStatus.Accept or AuthenticationStatus.Bypass) { _logger.LogDebug("Pre-auth post-check continued pipeline for '{user}' at '{domain}'.", context.RequestPacket.UserName, context.LdapSchema?.NamingContext.StringRepresentation); return Task.CompletedTask; } - context.ExecutionState.Terminate(); + context.Terminate(); _logger.LogDebug("Pre-auth post-check terminated pipeline for '{user}' at '{domain}'.", context.RequestPacket.UserName, context.LdapSchema?.NamingContext.StringRepresentation); return Task.CompletedTask; } diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/ProfileLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/ProfileLoadingStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs index baedd141..b032c89a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/ProfileLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs @@ -1,39 +1,38 @@ using Microsoft.Extensions.Logging; using Multifactor.Core.Ldap.Attributes; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class ProfileLoadingStep : IRadiusPipelineStep { - private readonly ILdapProfileService _ldapProfileService; + private readonly ILdapAdapter _ldapAdapter; private readonly ILogger _logger; private readonly ICacheService _cache; - public ProfileLoadingStep(ILdapProfileService ldapProfileService, ICacheService cache, ILogger logger) + public ProfileLoadingStep(ILdapAdapter ldapAdapter, ICacheService cache, ILogger logger) { - _ldapProfileService = ldapProfileService; + _ldapAdapter = ldapAdapter; _cache = cache; _logger = logger; } - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(ProfileLoadingStep)); if (ShouldSkipStep(context)) return Task.CompletedTask; - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration); if (string.IsNullOrWhiteSpace(context.RequestPacket.UserName)) { - var clientAddress = context.ProxyEndpoint?.Address.ToString() ?? context.RemoteEndpoint.Address.ToString(); + var clientAddress = context.RequestPacket.ProxyEndpoint?.Address.ToString() ?? context.RequestPacket.RemoteEndpoint.Address.ToString(); _logger.LogWarning("No user name provided in packet '{id}' from '{client}'", context.RequestPacket.Identifier, clientAddress); return Task.CompletedTask; } @@ -56,13 +55,13 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) throw new InvalidOperationException(); } - context.UserLdapProfile = profile; + context.LdapProfile = profile; _logger.LogInformation("Successfully found '{userIdentity}' profile at '{domain}'.", userIdentity.Identity, domain.StringRepresentation); return Task.CompletedTask; } - private ILdapProfile? TryGetUserProfile(UserIdentity userIdentity, DistinguishedName domain, LdapAttributeName[] attributes, IRadiusPipelineExecutionContext context) + private ILdapProfile? TryGetUserProfile(UserIdentity userIdentity, DistinguishedName domain, LdapAttributeName[] attributes, RadiusPipelineContext context) { var cacheKey = $"{userIdentity.Identity}-{domain.StringRepresentation}"; if (_cache.TryGetValue(cacheKey, out ILdapProfile? profile)) @@ -70,14 +69,26 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) _logger.LogDebug("Loaded '{user}' profile from cache.", userIdentity.Identity); return profile; } - - _logger.LogInformation("Try to find '{userIdentity}' profile at '{domain}'.", userIdentity.Identity, domain.StringRepresentation); - profile = _ldapProfileService.FindUserProfile(new FindUserProfileRequest(context.ClientConfigurationName, context.LdapServerConfiguration!, context.LdapSchema!, domain, userIdentity, attributes)); + var request = new FindUserRequest + { + ConnectionData = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }, + UserIdentity = userIdentity, + SearchBase = domain, + LdapSchema = context.LdapSchema, + AttributeNames = attributes, + }; + profile = _ldapAdapter.FindUserProfile(request); if (profile is null) return profile; - var expirationDate = DateTimeOffset.Now.AddHours(context.LdapServerConfiguration!.UserProfileCacheLifeTimeInHours); + var expirationDate = DateTimeOffset.Now.AddHours(0); //TODO context.LdapConfiguration!.UserProfileCacheLifeTimeInHours = 0 ???? SaveToCache(cacheKey, profile, expirationDate); _logger.LogDebug("'{userIdentity}' profile at '{domain}' is saved in cache till '{expirationDate}'.", userIdentity.Identity, domain.StringRepresentation, expirationDate.ToString()); @@ -85,19 +96,19 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) return profile; } - private IEnumerable GetAttributes(IRadiusPipelineExecutionContext context) + private static IList GetAttributes(RadiusPipelineContext context) { var attributes = new List() { new("memberOf"), new("userPrincipalName"), new("phone"), new("mail"), new("displayName"), new("email") }; - if (!string.IsNullOrWhiteSpace(context.LdapServerConfiguration!.IdentityAttribute)) - attributes.Add(new LdapAttributeName(context.LdapServerConfiguration.IdentityAttribute)); + if (!string.IsNullOrWhiteSpace(context.LdapConfiguration!.IdentityAttribute)) + attributes.Add(new LdapAttributeName(context.LdapConfiguration.IdentityAttribute)); - var replyAttributes = context.RadiusReplyAttributes.Values + var replyAttributes = context.ClientConfiguration.ReplyAttributes.Values .SelectMany(x => x) .Where(x => x.FromLdap) - .Select(x => new LdapAttributeName(x.LdapAttributeName)); + .Select(x => new LdapAttributeName(x.Name)); attributes.AddRange(replyAttributes); - attributes.AddRange(context.LdapServerConfiguration.PhoneAttributes.Select(x => new LdapAttributeName(x))); + attributes.AddRange(context.LdapConfiguration.PhoneAttributes.Select(x => new LdapAttributeName(x))); return attributes; } @@ -106,7 +117,7 @@ private void SaveToCache(string cacheKey, ILdapProfile profile, DateTimeOffset e _cache.Set(cacheKey, profile, expirationDate); } - private bool ShouldSkipStep(IRadiusPipelineExecutionContext context) + private bool ShouldSkipStep(RadiusPipelineContext context) { if (context.IsDomainAccount) return false; diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/SecondFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs similarity index 53% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/SecondFactorStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs index 30767269..0f63a440 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/SecondFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs @@ -1,55 +1,57 @@ using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class SecondFactorStep : IRadiusPipelineStep { - private readonly IMultifactorApiService _multifactorApiService; + private readonly MultifactorApiService _multifactorApiService; private readonly IChallengeProcessorProvider _challengeProcessorProvider; - private readonly ILdapGroupService _ldapGroupService; + private readonly ILdapAdapter _ldapAdapter; private readonly ILogger _logger; - public SecondFactorStep(IMultifactorApiService multifactorApiService, IChallengeProcessorProvider challengeProcessorProvider, ILdapGroupService ldapGroupService, ILogger logger) + public SecondFactorStep(MultifactorApiService multifactorApiService, IChallengeProcessorProvider challengeProcessorProvider, ILdapAdapter ldapAdapter, ILogger logger) { _multifactorApiService = multifactorApiService; _challengeProcessorProvider = challengeProcessorProvider; - _ldapGroupService = ldapGroupService; + _ldapAdapter = ldapAdapter; _logger = logger; } - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public async Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(SecondFactorStep)); ArgumentNullException.ThrowIfNull(context); if (!ShouldCallSecondFactor(context)) { - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Bypass; + context.SecondFactorStatus = AuthenticationStatus.Bypass; await Task.CompletedTask; return; } var shouldCacheApiResponse = ShouldCacheResponse(context); - var apiResponse = await _multifactorApiService.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context, shouldCacheApiResponse)); + var apiResponse = await _multifactorApiService.CreateSecondFactorRequestAsync(context, shouldCacheApiResponse); ProcessApiResponse(context, apiResponse); } - private bool ShouldCallSecondFactor(IRadiusPipelineExecutionContext context) + private bool ShouldCallSecondFactor(RadiusPipelineContext context) { - if (context.AuthenticationState.SecondFactorStatus != AuthenticationStatus.Awaiting) + if (context.SecondFactorStatus != AuthenticationStatus.Awaiting) return false; if (ShouldBypassByRequest(context)) { _logger.LogInformation("Second factor is bypassed for user '{user:l}' from {host:l}:{port}", context.RequestPacket.UserName, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); return false; } @@ -59,19 +61,19 @@ private bool ShouldCallSecondFactor(IRadiusPipelineExecutionContext context) if (!ShouldBypassByGroups(context)) return true; - _logger.LogInformation("Second factor is bypassed for user {user:l} at '{domain:l}'", context.RequestPacket.UserName, context.LdapServerConfiguration.ConnectionString); + _logger.LogInformation("Second factor is bypassed for user {user:l} at '{domain:l}'", context.RequestPacket.UserName, context.LdapConfiguration.ConnectionString); return false; } - private bool ShouldBypassByRequest(IRadiusPipelineExecutionContext context) + private static bool ShouldBypassByRequest(RadiusPipelineContext context) { - return context.RequestPacket.IsVendorAclRequest && context.FirstFactorAuthenticationSource == AuthenticationSource.Radius; + return context.RequestPacket.IsVendorAclRequest && context.ClientConfiguration.FirstFactorAuthenticationSource == AuthenticationSource.Radius; } - private bool ShouldBypassByGroups(IRadiusPipelineExecutionContext context) + private bool ShouldBypassByGroups(RadiusPipelineContext context) { - var serverConfig = context.LdapServerConfiguration; + var serverConfig = context.LdapConfiguration; if (serverConfig is null) return false; @@ -80,8 +82,8 @@ private bool ShouldBypassByGroups(IRadiusPipelineExecutionContext context) if (serverConfig.SecondFaBypassGroups.Any()) { - var request = new MembershipRequest(context, serverConfig.SecondFaBypassGroups); - bypassMember = _ldapGroupService.IsMemberOf(request); + var request = MembershipRequest.FromContext(context, serverConfig.SecondFaBypassGroups); + bypassMember = context.LdapProfile.MemberOf.Intersect(serverConfig.SecondFaBypassGroups).Any() || _ldapAdapter.IsMemberOf(request); } if (bypassMember is true) @@ -93,10 +95,10 @@ private bool ShouldBypassByGroups(IRadiusPipelineExecutionContext context) bool? secondFactorMember = null; if (serverConfig.SecondFaGroups.Any()) { - var request = new MembershipRequest(context, serverConfig.SecondFaGroups); - secondFactorMember = _ldapGroupService.IsMemberOf(request); - if (secondFactorMember is false) - _logger.LogInformation("User '{user:l}' is not a member of the 2FA group at '{domain:l}'", context.RequestPacket.UserName, serverConfig.ConnectionString); + var request = MembershipRequest.FromContext(context, serverConfig.SecondFaGroups); + secondFactorMember = context.LdapProfile.MemberOf.Intersect(serverConfig.SecondFaGroups).Any() || _ldapAdapter.IsMemberOf(request); + if (secondFactorMember is false) + _logger.LogInformation("User '{user:l}' is not a member of the 2FA group at '{domain:l}'", context.RequestPacket.UserName, serverConfig.ConnectionString); } if (secondFactorMember.HasValue) @@ -105,9 +107,9 @@ private bool ShouldBypassByGroups(IRadiusPipelineExecutionContext context) return false; } - private bool ShouldCacheResponse(IRadiusPipelineExecutionContext context) + private bool ShouldCacheResponse(RadiusPipelineContext context) { - if (context.LdapServerConfiguration is null || context.LdapServerConfiguration.AuthenticationCacheGroups.Count == 0) + if (context.LdapConfiguration is null || context.LdapConfiguration.AuthenticationCacheGroups.Count == 0) return true; if (!context.IsDomainAccount) @@ -118,10 +120,10 @@ private bool ShouldCacheResponse(IRadiusPipelineExecutionContext context) context.RequestPacket.AccountType); return false; } - - var cacheGroups = context.LdapServerConfiguration.AuthenticationCacheGroups; - var isMember = _ldapGroupService.IsMemberOf(new MembershipRequest(context, cacheGroups)); - var groupsStr = string.Join(',', cacheGroups); + + var request = MembershipRequest.FromContext(context, context.LdapConfiguration.AuthenticationCacheGroups); + var isMember = context.LdapProfile.MemberOf.Intersect(context.LdapConfiguration.AuthenticationCacheGroups).Any() || _ldapAdapter.IsMemberOf(request); + var groupsStr = string.Join(',', context.LdapConfiguration.AuthenticationCacheGroups); var username = context.RequestPacket.UserName; if (!isMember) _logger.LogDebug("User '{userName}' is not a member of any authentication cache groups: ({groups})", username, groupsStr); @@ -131,9 +133,9 @@ private bool ShouldCacheResponse(IRadiusPipelineExecutionContext context) return isMember; } - private void ProcessApiResponse(IRadiusPipelineExecutionContext context, MultifactorResponse apiResponse) + private void ProcessApiResponse(RadiusPipelineContext context, SecondFactorResponse apiResponse) { - context.AuthenticationState.SecondFactorStatus = apiResponse.Code; + context.SecondFactorStatus = apiResponse.Code; context.ResponseInformation.State = apiResponse.State; context.ResponseInformation.ReplyMessage = apiResponse.ReplyMessage; @@ -146,7 +148,7 @@ private void ProcessApiResponse(IRadiusPipelineExecutionContext context, Multifa challengeProcessor.AddChallengeContext(context); } - private bool UnsupportedAccountType(IRadiusPipelineExecutionContext context) + private bool UnsupportedAccountType(RadiusPipelineContext context) { if (context.IsDomainAccount) return false; diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/StatusServerFilteringStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs similarity index 50% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/StatusServerFilteringStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs index 2ba3a5df..7738acfa 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/StatusServerFilteringStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class StatusServerFilteringStep : IRadiusPipelineStep { @@ -16,7 +16,7 @@ public StatusServerFilteringStep(ApplicationVariables applicationVariables, ILog _logger = logger; } - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public async Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(StatusServerFilteringStep)); var packet = context.RequestPacket; @@ -27,9 +27,9 @@ public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) } var uptime = _applicationVariables.UpTime; - context.ResponseInformation.ReplyMessage = $"Server up {uptime.Days} days {uptime:hh\\:mm\\:ss}, ver.: {_applicationVariables.AppVersion}"; - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - context.ExecutionState.Terminate(); + context.ResponseInformation.ReplyMessage = $@"Server up {uptime.Days} days {uptime:hh\:mm\:ss}, ver.: {_applicationVariables.AppVersion}"; + context.FirstFactorStatus = AuthenticationStatus.Accept; + context.SecondFactorStatus = AuthenticationStatus.Accept; + context.Terminate(); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs new file mode 100644 index 00000000..ea7af5d0 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs @@ -0,0 +1,150 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; + +public class UserGroupLoadingStep : IRadiusPipelineStep +{ + private readonly ILdapAdapter _ldapAdapter; + private readonly ILogger _logger; + + public UserGroupLoadingStep(ILdapAdapter ldapAdapter, ILogger logger) + { + _ldapAdapter = ldapAdapter; + _logger = logger; + } + + public Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("'{name}' started", nameof(UserGroupLoadingStep)); + + if (ShouldSkipGroupLoading(context)) + return Task.CompletedTask; + + ArgumentNullException.ThrowIfNull(context.LdapProfile, nameof(context.LdapProfile)); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration, nameof(context.LdapConfiguration)); + + var userGroups = new HashSet(); + + foreach (var group in context.LdapProfile.MemberOf.Select(x => x.Components.Deepest.Value)) + userGroups.Add(group); + + context.UserGroups = userGroups; + + if (!context.LdapConfiguration.LoadNestedGroups) + { + _logger.LogDebug("Nested groups for {domain} are not required.", context.LdapConfiguration.ConnectionString); + return Task.CompletedTask; + } + + LoadGroupsFromLdapCatalog(context, userGroups); + + return Task.CompletedTask; + } + + private void LoadGroupsFromLdapCatalog(RadiusPipelineContext context, HashSet userGroups) + { + + if (context.LdapConfiguration!.NestedGroupsBaseDns.Count > 0) + LoadUserGroupsFromContainers(context, userGroups); + else + LoadUserGroupsFromRoot(context, userGroups); + } + + private void LoadUserGroupsFromContainers(RadiusPipelineContext context, HashSet userGroups) + { + foreach (var dn in context.LdapConfiguration!.NestedGroupsBaseDns) + { + _logger.LogDebug("Loading nested groups from '{dn}' at '{domain}' for '{user}'", dn, context.LdapConfiguration.ConnectionString, context.RequestPacket.UserName); + + var request = new LoadUserGroupRequest + { + ConnectionData = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }, + LdapSchema = context.LdapSchema!, + UserDN = context.LdapProfile!.Dn, + SearchBase = dn + }; + + var groups = _ldapAdapter.LoadUserGroups(request); + var groupLog = string.Join("\n", groups); + _logger.LogDebug("Found groups at '{domain}' for '{user}': {groups}", dn, context.RequestPacket.UserName, groupLog); + + foreach (var group in groups) + userGroups.Add(group); + } + } + + private void LoadUserGroupsFromRoot(RadiusPipelineContext context, HashSet userGroups) + { + var request = new LoadUserGroupRequest + { + ConnectionData = new LdapConnectionData + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }, + LdapSchema = context.LdapSchema!, + UserDN = context.LdapProfile!.Dn + }; + + _logger.LogDebug("Loading nested groups from root at '{domain}' for '{user}'", context.LdapConfiguration!.ConnectionString, context.RequestPacket.UserName); + var groups = _ldapAdapter.LoadUserGroups(request); + + var groupLog = string.Join("\n", groups); + _logger.LogDebug("Found groups at root for '{user}': {groups}", context.RequestPacket.UserName, groupLog); + foreach (var group in groups) + userGroups.Add(group); + } + + private bool ShouldSkipGroupLoading(RadiusPipelineContext context) + { + return !AcceptedRequest(context) || GroupsNotRequired(context) || UnsupportedAccountType(context); + } + + private bool GroupsNotRequired(RadiusPipelineContext context) + { + var notRequired = !context + .ClientConfiguration.ReplyAttributes + .Values + .SelectMany(x => x) + .Any(x => x.IsMemberOf || x.UserGroupCondition.Count > 0); + + if (!notRequired) + return false; + + _logger.LogDebug("User groups are not required."); + return true; + } + + private bool UnsupportedAccountType(RadiusPipelineContext context) + { + if (context.IsDomainAccount) + return false; + + _logger.LogInformation( + "User '{user}' used '{accountType}' account to log in. User group loading is skipped.", + context.RequestPacket.UserName, + context.RequestPacket.AccountType); + + return true; + } + + private static bool AcceptedRequest(RadiusPipelineContext context) + { + return context.FirstFactorStatus is + AuthenticationStatus.Accept or AuthenticationStatus.Bypass + && context.SecondFactorStatus is + AuthenticationStatus.Accept or AuthenticationStatus.Bypass; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserNameValidationStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs similarity index 51% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserNameValidationStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs index 246fc530..5315afe7 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserNameValidationStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class UserNameValidationStep : IRadiusPipelineStep { @@ -15,7 +13,7 @@ public UserNameValidationStep(ILogger logger) _logger = logger; } - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(UserNameValidationStep)); @@ -26,7 +24,7 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) return Task.CompletedTask; } - var serverSettings = context.LdapServerConfiguration; + var serverSettings = context.LdapConfiguration; if (serverSettings is null) { @@ -36,7 +34,7 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) var identity = new UserIdentity(userName); - if (serverSettings.UpnRequired && identity.Format != UserIdentityFormat.UserPrincipalName) + if (serverSettings.RequiresUpn && identity.Format != UserIdentityFormat.UserPrincipalName) { TerminateWithError(context, "User name in UPN format is required."); _logger.LogWarning("User name in UPN format is required. Provided name: {name}", userName); @@ -46,23 +44,33 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) if (identity.Format != UserIdentityFormat.UserPrincipalName) return Task.CompletedTask; - var suffix = Utils.GetUpnSuffix(identity); - var isPermitted = serverSettings.SuffixesPermissions.IsPermitted(suffix); - if (!isPermitted) + if (!IsPermittedSuffix(identity.GetUpnSuffix(), serverSettings.IncludedSuffixes, serverSettings.ExcludedSuffixes)) { TerminateWithError(context, "UPN suffix is not permitted."); _logger.LogWarning("UPN suffix is not permitted. Provided name: {name}", userName); - return Task.CompletedTask; } return Task.CompletedTask; } + + private static bool IsPermittedSuffix(string domain, IReadOnlyList includedSuffixes, IReadOnlyList excludedSuffixes) + { + if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain)); + + if (includedSuffixes != null && includedSuffixes.Count > 0) + return includedSuffixes.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); + + if (excludedSuffixes != null && excludedSuffixes.Count > 0) + return excludedSuffixes.All(excluded => !excluded.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); + + return true; + } - private void TerminateWithError(IRadiusPipelineExecutionContext context, string replyMessage) + private static void TerminateWithError(RadiusPipelineContext context, string replyMessage) { - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Awaiting; - context.ExecutionState.Terminate(); + context.FirstFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Awaiting; context.ResponseInformation.ReplyMessage = replyMessage; + context.Terminate(); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs new file mode 100644 index 00000000..109a864c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs @@ -0,0 +1,12 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; + +public class PipelineNotFoundException : Exception +{ + public string ClientName { get; } + + public PipelineNotFoundException(string message, string clientName) + : base(message) + { + ClientName = clientName; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusPacketException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusPacketException.cs new file mode 100644 index 00000000..2a2fa91e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusPacketException.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; + +public class RadiusPacketException: Exception +{ + public RadiusPacketException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/AccountType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AccountType.cs similarity index 53% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/AccountType.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AccountType.cs index cc5b4ac9..1a90f627 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/AccountType.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AccountType.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; public enum AccountType { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/AuthenticationType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AuthenticationType.cs similarity index 63% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/AuthenticationType.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AuthenticationType.cs index 078ea0e9..04815fc4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/AuthenticationType.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/AuthenticationType.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums { public enum AuthenticationType { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/PacketCode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/PacketCode.cs similarity index 85% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/PacketCode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/PacketCode.cs index b5b2cd88..df07c96a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/PacketCode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums/PacketCode.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums { // See https://datatracker.ietf.org/doc/html/rfc2865#section-3 public enum PacketCode diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs new file mode 100644 index 00000000..06efc820 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +public class GetReplyAttributesRequest +{ + public string? UserName { get; } + public HashSet UserGroups { get; } + public IReadOnlyDictionary> ReplyAttributes { get; } + private IReadOnlyCollection Attributes { get; } + + public GetReplyAttributesRequest( + string? userName, + HashSet userGroups, + IReadOnlyDictionary> replyAttributes, + IReadOnlyCollection userAttributes) + { + ArgumentNullException.ThrowIfNull(userGroups); + ArgumentNullException.ThrowIfNull(replyAttributes); + ArgumentNullException.ThrowIfNull(userAttributes); + + UserName = userName; + UserGroups = userGroups; + ReplyAttributes = replyAttributes; + Attributes = userAttributes; + } + + public bool HasAttribute(string attributeName) + { + var attribute = Attributes.FirstOrDefault(x => x.Name.Value.ToLower(CultureInfo.InvariantCulture) == attributeName.ToLower(CultureInfo.InvariantCulture)); + return attribute is not null; + } + + public string[] GetAttributeValues(string attributeName) + { + var attribute = Attributes.FirstOrDefault(x => x.Name.Value.ToLower(CultureInfo.InvariantCulture) == attributeName.ToLower(CultureInfo.InvariantCulture)); + return attribute?.GetNotEmptyValues() ?? []; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/ParsedAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/ParsedAttribute.cs new file mode 100644 index 00000000..69a078f8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/ParsedAttribute.cs @@ -0,0 +1,15 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +public class ParsedAttribute +{ + public string Name { get; } + public object Value { get; } + public bool IsMessageAuthenticator { get; } + + public ParsedAttribute(string name, object value, bool isMessageAuthenticator = false) + { + Name = name; + Value = value; + IsMessageAuthenticator = isMessageAuthenticator; + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAttribute.cs similarity index 89% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAttribute.cs index 36262ceb..f0664829 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAttribute.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; /// /// Radius attribute model diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAuthenticator.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAuthenticator.cs similarity index 69% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAuthenticator.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAuthenticator.cs index d5c136bc..190bb17f 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusAuthenticator.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusAuthenticator.cs @@ -1,9 +1,9 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Metadata; - -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models { public class RadiusAuthenticator { + private const int AuthenticatorFieldLength = 16; + private const int AuthenticatorFieldPosition = 4; public byte[] Value { get; } public RadiusAuthenticator() @@ -33,10 +33,10 @@ public static RadiusAuthenticator Parse(byte[] packetBytes) throw new ArgumentNullException(nameof(packetBytes)); } - var authenticator = new byte[RadiusFieldOffsets.AuthenticatorFieldLength]; - Buffer.BlockCopy(packetBytes, RadiusFieldOffsets.AuthenticatorFieldPosition, authenticator, 0, RadiusFieldOffsets.AuthenticatorFieldLength); + var authenticator = new byte[AuthenticatorFieldLength]; + Buffer.BlockCopy(packetBytes, AuthenticatorFieldPosition, authenticator, 0, AuthenticatorFieldLength); return new RadiusAuthenticator(authenticator); } } -} +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacket.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs similarity index 93% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacket.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs index 3915e72c..b8a164b1 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacket.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs @@ -1,10 +1,12 @@ using System.Net; using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; // See https://datatracker.ietf.org/doc/html/rfc2865#section-3 to understand class structure -public class RadiusPacket : IRadiusPacket +public class RadiusPacket { private string? UserPassword => GetAttributeValueAsString("User-Password"); private readonly Dictionary _attributes = new(); @@ -106,7 +108,7 @@ public RadiusPacket(RadiusPacketHeader header, RadiusAuthenticator? requestAuthe if ((parts?.Length ?? 0) < 2) return null; - password = parts![1].Base64toUtf8(); + password = parts![1].FromBase64ToUtf8(); return password; } @@ -125,7 +127,13 @@ public RadiusPacket(RadiusPacketHeader header, RadiusAuthenticator? requestAuthe if ((parts?.Length ?? 0) < 3) return null; - return parts![2].Base64toUtf8(); + return parts![2].FromBase64ToUtf8(); + } + + + public bool HasAttribute(string name) + { + return _attributes.ContainsKey(name); } /// @@ -194,7 +202,7 @@ public List GetAttributes(string name) public string CreateUniqueKey(IPEndPoint remoteEndpoint) { - var base64Authenticator = Authenticator.Value.Base64(); + var base64Authenticator = Authenticator.Value.ToBase64(); return $"{Code:d}:{Identifier}:{remoteEndpoint}:{UserName}:{base64Authenticator}"; } diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacketHeader.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs similarity index 65% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacketHeader.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs index 49c47186..60b79bb9 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/RadiusPacketHeader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs @@ -1,8 +1,7 @@ using System.Security.Cryptography; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Radius.Metadata; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models { /// /// Radius packet header model @@ -10,13 +9,18 @@ namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet /// public class RadiusPacketHeader { + public const int CodeFieldPosition = 0; + public const int IdentifierFieldPosition = 1; + public const int AuthenticatorFieldPosition = 4; + public const int AuthenticatorFieldLength = 16; public PacketCode Code { get; } public byte Identifier { get; } public RadiusAuthenticator Authenticator { get; } + public RadiusPacketHeader(){} public RadiusPacketHeader(PacketCode code, byte identifier, byte[] authenticator) { - Throw.IfNull(authenticator, nameof(authenticator)); + ArgumentNullException.ThrowIfNull(authenticator, nameof(authenticator)); Code = code; Identifier = identifier; @@ -37,18 +41,18 @@ public static RadiusPacketHeader Parse(byte[] packet) throw new ArgumentNullException(nameof(packet)); } - var code = (PacketCode)packet[RadiusFieldOffsets.CodeFieldPosition]; - var identifier = packet[RadiusFieldOffsets.IdentifierFieldPosition]; + var code = (PacketCode)packet[CodeFieldPosition]; + var identifier = packet[IdentifierFieldPosition]; - var authenticator = new byte[RadiusFieldOffsets.AuthenticatorFieldLength]; - Buffer.BlockCopy(packet, RadiusFieldOffsets.AuthenticatorFieldPosition, authenticator, 0, RadiusFieldOffsets.AuthenticatorFieldLength); + var authenticator = new byte[AuthenticatorFieldLength]; + Buffer.BlockCopy(packet, AuthenticatorFieldPosition, authenticator, 0, AuthenticatorFieldLength); return new RadiusPacketHeader(code, identifier, authenticator); } public static RadiusPacketHeader Create(PacketCode code, byte identifier) { - var auth = new byte[RadiusFieldOffsets.AuthenticatorFieldLength]; + var auth = new byte[AuthenticatorFieldLength]; // Generate random authenticator for access request packets if (code == PacketCode.AccessRequest || code == PacketCode.StatusServer) { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs new file mode 100644 index 00000000..7a243340 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs @@ -0,0 +1,47 @@ +using System.Net; +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +public class SendAdapterResponseRequest +{ + public bool ShouldSkipResponse { get; set; } + public RadiusPacket? ResponsePacket { get; set; } + public RadiusPacket RequestPacket { get; set; } + public IPEndPoint RemoteEndpoint { get; set; } + public IPEndPoint? ProxyEndpoint { get; set; } + public AuthenticationStatus FirstFactorStatus { get; set; } + + public AuthenticationStatus SecondFactorStatus { get; set; } + public ResponseInformation ResponseInformation { get; set; } + public SharedSecret RadiusSharedSecret { get; set; } + public HashSet UserGroups { get; set; } + public IReadOnlyDictionary> RadiusReplyAttributes { get; set; } + public IReadOnlyCollection Attributes { get; set; } + public CredentialDelay? InvalidCredentialDelay { get; set; } + + + public static SendAdapterResponseRequest FromContext(RadiusPipelineContext context) + { + return new SendAdapterResponseRequest + { + ShouldSkipResponse = context.ShouldSkipResponse, + ResponsePacket = context.ResponsePacket, + RequestPacket = context.RequestPacket, + RemoteEndpoint = context.RequestPacket.RemoteEndpoint, + ProxyEndpoint = context.RequestPacket.ProxyEndpoint, + FirstFactorStatus = context.FirstFactorStatus, + SecondFactorStatus = context.SecondFactorStatus, + ResponseInformation = context.ResponseInformation, + RadiusSharedSecret = new SharedSecret(context.ClientConfiguration.RadiusSharedSecret), + UserGroups = context.UserGroups, + RadiusReplyAttributes = context.ClientConfiguration.ReplyAttributes, + Attributes = context.LdapProfile?.Attributes ?? [], + InvalidCredentialDelay = context.ClientConfiguration.InvalidCredentialDelay + }; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs new file mode 100644 index 00000000..61e8a195 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs @@ -0,0 +1,60 @@ +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md + +//MIT License + +//Copyright(c) 2017 Verner Fortelius + +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: + +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models +{ + public class SharedSecret + { + public byte[] Bytes { get; } + + public SharedSecret(string secret) + { + if (string.IsNullOrWhiteSpace(secret)) + { + throw new ArgumentException($"'{nameof(secret)}' cannot be null or whitespace.", nameof(secret)); + } + + Bytes = Encoding.UTF8.GetBytes(secret); + } + + public SharedSecret(byte[] secret) + { + if (secret is null) + { + throw new ArgumentNullException(nameof(secret)); + } + + if (secret.Length == 0) + { + throw new ArgumentException("Empty secret", nameof(secret)); + } + + Bytes = secret; + } + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClient.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClient.cs similarity index 71% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClient.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClient.cs index 057f7bc9..382cbd59 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClient.cs @@ -1,6 +1,6 @@ using System.Net; -namespace Multifactor.Radius.Adapter.v2.Services.Radius; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; public interface IRadiusClient : IDisposable { diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClientFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClientFactory.cs similarity index 62% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClientFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClientFactory.cs index 1c88dbdf..b2ce93ca 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusClientFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusClientFactory.cs @@ -1,6 +1,6 @@ using System.Net; -namespace Multifactor.Radius.Adapter.v2.Services.Radius; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; public interface IRadiusClientFactory { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusPacketService.cs new file mode 100644 index 00000000..cead290e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusPacketService.cs @@ -0,0 +1,13 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +public interface IRadiusPacketService +{ + // Только высокоуровневые операции + RadiusPacket ParsePacket(byte[] packetBytes, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null); + byte[] SerializePacket(RadiusPacket packet, SharedSecret sharedSecret); + RadiusPacket CreateResponse(RadiusPacket request, PacketCode responseCode); + bool TryGetNasIdentifier(byte[] packetBytes, out string nasIdentifier); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusUdpAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusUdpAdapter.cs new file mode 100644 index 00000000..8563c51a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusUdpAdapter.cs @@ -0,0 +1,8 @@ +using System.Net.Sockets; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +public interface IRadiusUdpAdapter +{ + Task Handle(UdpReceiveResult udpPacket); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IResponseSender.cs new file mode 100644 index 00000000..72536c1f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IResponseSender.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +public interface IResponseSender +{ + Task SendResponse(SendAdapterResponseRequest context); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs similarity index 51% rename from src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs index 197b10a1..98587eea 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs @@ -1,10 +1,10 @@ using System.Net; using System.Net.Sockets; -namespace Multifactor.Radius.Adapter.v2.Server.Udp; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; public interface IUdpClient : IDisposable { Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint); - Task ReceiveAsync(); + Task ReceiveAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusAttributeTypeConverter.cs new file mode 100644 index 00000000..736a8edb --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusAttributeTypeConverter.cs @@ -0,0 +1,6 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +public interface IRadiusAttributeTypeConverter +{ + object ConvertType(string attributeName, object value); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs new file mode 100644 index 00000000..9a24d340 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +public interface IRadiusPacketProcessor +{ + Task ProcessPacketAsync(RadiusPacket requestPacket, IClientConfiguration clientConfiguration); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusReplyAttributeService.cs new file mode 100644 index 00000000..6c5a6c60 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusReplyAttributeService.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +public interface IRadiusReplyAttributeService +{ + IDictionary> GetReplyAttributes(GetReplyAttributesRequest request); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs new file mode 100644 index 00000000..1d3c7a76 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs @@ -0,0 +1,165 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +public class RadiusPacketProcessor : IRadiusPacketProcessor +{ + private readonly IPipelineProvider _pipelineProvider; + private readonly IResponseSender _responseSender; + private readonly ILogger _logger; + + public RadiusPacketProcessor( + IPipelineProvider pipelineProvider, + IResponseSender responseSender, + ILogger logger) + { + _pipelineProvider = pipelineProvider ?? throw new ArgumentNullException(nameof(pipelineProvider)); + _responseSender = responseSender ?? throw new ArgumentNullException(nameof(responseSender)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ProcessPacketAsync(RadiusPacket requestPacket, IClientConfiguration clientConfiguration) + { + ArgumentNullException.ThrowIfNull(requestPacket); + ArgumentNullException.ThrowIfNull(clientConfiguration); + + _logger.LogDebug("Start processing '{PacketType}' packet for client '{ClientName}'.", + requestPacket.Code, clientConfiguration.Name); + + if (ShouldProcessWithoutLdap(requestPacket, clientConfiguration)) + { + await ExecutePipeline(clientConfiguration, requestPacket); + return; + } + + await TryProcessWithLdapServers(clientConfiguration, requestPacket); + } + + private async Task TryProcessWithLdapServers(IClientConfiguration clientConfiguration, RadiusPacket requestPacket) + { + var processedSuccessfully = false; + Exception? lastException = null; + + foreach (var serverConfig in clientConfiguration.LdapServers) + { + try + { + var success = await ExecutePipeline(clientConfiguration, requestPacket, serverConfig); + if (success) + { + processedSuccessfully = true; + break; + } + } + catch (Exception ex) + { + lastException = ex; + _logger.LogWarning(ex, + "Failed to process with LDAP server {ConnectionString} for client {ClientName}", + serverConfig.ConnectionString, clientConfiguration.Name); + } + } + + if (!processedSuccessfully) + { + _logger.LogError(lastException, + "All LDAP servers failed for client {ClientName}", clientConfiguration.Name); + throw new Exception( + $"All LDAP servers failed for client '{clientConfiguration.Name}'", + lastException); + } + } + + private async Task ExecutePipeline( + IClientConfiguration clientConfiguration, + RadiusPacket requestPacket, + ILdapServerConfiguration? ldapServerConfiguration = null) + { + if (ldapServerConfiguration != null) + { + _logger.LogDebug( + "Executing pipeline for client {ClientName} with LDAP server {ConnectionString}", + clientConfiguration.Name, ldapServerConfiguration.ConnectionString); + } + else + { + _logger.LogDebug( + "Executing pipeline for client {ClientName}", + clientConfiguration.Name); + } + + var context = CreatePipelineContext(clientConfiguration, requestPacket, ldapServerConfiguration); + var pipeline = GetPipeline(clientConfiguration); + + try + { + await pipeline.ExecuteAsync(context); + + var responseRequest = SendAdapterResponseRequest.FromContext(context); + await _responseSender.SendResponse(responseRequest); + + return true; + } + catch (PipelineNotFoundException ex) + { + _logger.LogError(ex, "Pipeline configuration error for client {ClientName}", clientConfiguration.Name); + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "Pipeline execution failed for client {ClientName}{ServerInfo}", + clientConfiguration.Name, + ldapServerConfiguration != null ? $" with LDAP server {ldapServerConfiguration.ConnectionString}" : ""); + throw; + } + } + + private static RadiusPipelineContext CreatePipelineContext( + IClientConfiguration clientConfiguration, + RadiusPacket requestPacket, + ILdapServerConfiguration? ldapServerConfiguration = null) + { + + var password = requestPacket.TryGetUserPassword(); + var passphrase = UserPassphrase.Parse(password, clientConfiguration.PreAuthenticationMethod); + + var context = new RadiusPipelineContext(requestPacket, clientConfiguration, ldapServerConfiguration) + { + Passphrase = passphrase + }; + + return context; + } + + private IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration) + { + var pipeline = _pipelineProvider.GetPipeline(clientConfiguration); + if (pipeline is null) + { + throw new PipelineNotFoundException( + $"No pipeline found for client '{clientConfiguration.Name}'. " + + "Check adapter configuration and restart the adapter.", + clientConfiguration.Name); + } + return pipeline; + } + + private static bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, IClientConfiguration clientConfiguration) + { + if (clientConfiguration.LdapServers.Count <= 0) + return true; + + if (requestPacket.Code != PacketCode.AccessRequest) + return true; + + return false; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs new file mode 100644 index 00000000..c6579fa5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs @@ -0,0 +1,53 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Security; + +public static class ProtectionService +{ + public static string Protect(string secret, string data) + { + ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); + + var bytes = StringToBytes(data); + if (OperatingSystem.IsWindows()) + { + var additionalEntropy = StringToBytes(secret); + return ToBase64(ProtectedData.Protect(bytes, additionalEntropy, DataProtectionScope.CurrentUser)); + } + return ToBase64(bytes); + } + + public static string Unprotect(string secret, string data) + { + ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); + + var bytes = FromBase64(data); + if (OperatingSystem.IsWindows()) + { + var additionalEntropy = StringToBytes(secret); + return BytesToString(ProtectedData.Unprotect(bytes, additionalEntropy, DataProtectionScope.CurrentUser)); + } + return BytesToString(bytes); + } + + private static byte[] StringToBytes(string s) + { + return Encoding.UTF8.GetBytes(s); + } + + private static string BytesToString(byte[] b) + { + return Encoding.UTF8.GetString(b); + } + + private static string ToBase64(byte[] data) + { + return Convert.ToBase64String(data); + } + + private static byte[] FromBase64(string text) + { + return Convert.FromBase64String(text); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs new file mode 100644 index 00000000..0c4a61da --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs @@ -0,0 +1,94 @@ +using System.Security.Cryptography; +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Security +{ + public static class RadiusPasswordProtector + { + /// + /// Encrypt/decrypt using XOR + /// + /// + /// + /// + private static byte[] EncryptDecrypt(byte[] input, byte[] key) + { + var output = new byte[input.Length]; + for (int i = 0; i < input.Length; i++) + { + output[i] = (byte)(input[i] ^ key[i]); + } + return output; + } + + + /// + /// Create a radius shared secret key + /// + /// + /// + /// + private static byte[] CreateKey(SharedSecret sharedSecret, RadiusAuthenticator authenticator) + { + var key = new byte[16 + sharedSecret.Bytes.Length]; + Buffer.BlockCopy(sharedSecret.Bytes, 0, key, 0, sharedSecret.Bytes.Length); + Buffer.BlockCopy(authenticator.Value, 0, key, sharedSecret.Bytes.Length, authenticator.Value.Length); + return MD5.HashData(key); + } + + + /// + /// Decrypt user password + /// + /// + /// + /// + /// + public static string Decrypt(SharedSecret sharedSecret, RadiusAuthenticator authenticator, byte[] passwordBytes) + { + var key = CreateKey(sharedSecret, authenticator); + var bytes = new List(); + + for (var n = 1; n <= passwordBytes.Length / 16; n++) + { + var temp = new byte[16]; + Buffer.BlockCopy(passwordBytes, (n - 1) * 16, temp, 0, 16); + + var block = EncryptDecrypt(temp, key); + bytes.AddRange(block); + + key = CreateKey(sharedSecret, new RadiusAuthenticator(temp)); + } + + var ret = Encoding.UTF8.GetString(bytes.ToArray()); + return ret.Replace("\0", ""); + } + + + /// + /// Encrypt a password + /// + /// + /// + /// + /// + public static byte[] Encrypt(SharedSecret sharedSecret, RadiusAuthenticator authenticator, byte[] passwordBytes) + { + Array.Resize(ref passwordBytes, passwordBytes.Length + (16 - passwordBytes.Length % 16)); + + var key = CreateKey(sharedSecret, authenticator); + var bytes = new List(); + for (var n = 1; n <= passwordBytes.Length / 16; n++) + { + var temp = new byte[16]; + Buffer.BlockCopy(passwordBytes, (n - 1) * 16, temp, 0, 16); + var xor = EncryptDecrypt(temp, key); + bytes.AddRange(xor); + key = CreateKey(sharedSecret, new RadiusAuthenticator(xor)); + } + + return bytes.ToArray(); + } + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Multifactor.Radius.Adapter.v2.Application.csproj b/src/Multifactor.Radius.Adapter.v2.Application/Multifactor.Radius.Adapter.v2.Application.csproj new file mode 100644 index 00000000..016ba518 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Multifactor.Radius.Adapter.v2.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Constants/RadiusAdapterConstants.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Constants/RadiusAdapterConstants.cs deleted file mode 100644 index 4f786b31..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Constants/RadiusAdapterConstants.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/E2EClientConfigurationsProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs deleted file mode 100644 index 2f63b4f9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/E2ETestBase.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs deleted file mode 100644 index 66536469..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.DirectoryServices.Protocols; -using System.Reflection; -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; -using Multifactor.Radius.Adapter.v2.Extensions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Server; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; - -namespace Multifactor.Radius.Adapter.v2.EndToEndTests; - -public abstract class E2ETestBase(RadiusFixtures radiusFixtures) : IDisposable -{ - private IHost? _host; - private IClientConfigurationFactory? _clientConfigurationFactory; - private IRadiusPacketService _radiusPacketService = radiusFixtures.Parser; - private readonly SharedSecret _secret = radiusFixtures.SharedSecret; - private readonly UdpSocket _udpSocket = radiusFixtures.UdpSocket; - - private protected async Task StartHostAsync( - RadiusAdapterConfiguration rootConfig, - Dictionary? clientConfigs = null, - Action? configure = null) - { - var builder = Host.CreateApplicationBuilder(["--environment", "Test"]); - builder.Services.AddMemoryCache(); - builder.Services.AddAdapterLogging(); - - var appVars = new ApplicationVariables - { - AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), - AppVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(), - StartedAt = DateTime.Now - }; - - builder.Services.AddSingleton(appVars); - builder.Services.AddRadiusDictionary(); - builder.Services.AddConfiguration(); - - builder.Services.ReplaceService(prov => - { - var factory = prov.GetRequiredService(); - - var config = factory.CreateConfig(rootConfig); - - return config; - }); - - var clientConfigsProvider = new E2EClientConfigurationsProvider(clientConfigs); - builder.Services.ReplaceService(clientConfigsProvider); - builder.Services.AddLdapSchemaLoader(); - builder.Services.AddDataProtectionService(); - - builder.Services.AddFirstFactor(); - builder.Services.AddPipelines(); - - builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddServices(); - builder.Services.AddChallenge(); - builder.Services.AddUdpClient(); - builder.Services.AddMultifactorHttpClient(); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(); - - configure?.Invoke(builder); - - _host = builder.Build(); - - _clientConfigurationFactory = _host.Services.GetService(); - - await _host.StartAsync(); - } - - protected IRadiusPacket SendPacketAsync(IRadiusPacket? radiusPacket, SharedSecret? secret = null) - { - ArgumentNullException.ThrowIfNull(radiusPacket); - - var packetBytes = _radiusPacketService.GetBytes(radiusPacket, secret ?? _secret); - _udpSocket.Send(packetBytes); - - var data = _udpSocket.Receive(); - var parsed = _radiusPacketService.Parse(data.GetBytes(), secret ?? _secret, radiusPacket.Authenticator); - - return parsed; - } - - protected RadiusPacket CreateRadiusPacket(PacketCode packetCode, byte identifier = 0) - { - RadiusPacket packet; - switch (packetCode) - { - case PacketCode.AccessRequest: - packet = RadiusPacketFactory.AccessRequest(identifier); - break; - case PacketCode.StatusServer: - packet = RadiusPacketFactory.StatusServer(identifier); - break; - case PacketCode.AccessChallenge: - packet = RadiusPacketFactory.AccessChallenge(identifier); - break; - case PacketCode.AccessReject: - packet = RadiusPacketFactory.AccessReject(identifier); - break; - default: - throw new NotImplementedException(); - } - - return packet; - } - - protected void SetAttributeForUserInCatalogAsync( - DistinguishedName userDn, - RadiusAdapterConfiguration config, - string attributeName, - object attributeValue) - { - ArgumentNullException.ThrowIfNull(_host); - - var clientConfiguration = CreateClientConfiguration(config); - var connectionFactory = _host.Services.GetRequiredService(); - var serverConfig = clientConfiguration.LdapServers.First(); - - using var connection = connectionFactory.CreateConnection(new LdapConnectionOptions( - new LdapConnectionString(serverConfig.ConnectionString), - AuthType.Basic, - serverConfig.UserName, - serverConfig.Password, - TimeSpan.FromSeconds(serverConfig.BindTimeoutInSeconds))); - - var request = BuildModifyRequest(userDn, attributeName, attributeValue); - var response = connection.SendRequest(request); - - if (response.ResultCode != ResultCode.Success) - throw new Exception($"Failed to set attribute: {response.ResultCode}"); - } - - protected IClientConfiguration CreateClientConfiguration(RadiusAdapterConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(_host); - ArgumentNullException.ThrowIfNull(_clientConfigurationFactory); - - var serviceConfig = _host.Services.GetService(); - return _clientConfigurationFactory.CreateConfig("e2e", configuration, serviceConfig!); - } - - private ModifyRequest BuildModifyRequest( - DistinguishedName dn, - string attributeName, - object attributeValue) - { - var attribute = new DirectoryAttributeModification - { - Name = attributeName, - Operation = DirectoryAttributeOperation.Replace - }; - - var bytes = Encoding.UTF8.GetBytes(attributeValue.ToString()); - attribute.Add(bytes); - - return new ModifyRequest(dn.StringRepresentation, attribute); - } - - public void Dispose() - { - _host?.StopAsync(); - _host?.Dispose(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs deleted file mode 100644 index 0aed38d7..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.EndToEndTests; - -internal static class E2ETestsUtils -{ - internal static IRadiusPacketService GetRadiusPacketParser() - { - var appVar = new ApplicationVariables - { - AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) ?? throw new Exception() - }; - var dict = new RadiusDictionary(appVar); - dict.Read(); - - return new RadiusPacketService(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.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs deleted file mode 100644 index a7ea261c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.Extensions.Options; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.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); - _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); - _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.v2.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs deleted file mode 100644 index 3bc320c8..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.ConfigLoading; - -internal class TestConfigProviderOptions -{ - public string? RootConfigFilePath { get; set; } - public string? ClientConfigsFolderPath { get; set; } - public string[] ClientConfigFilePaths { get; set; } = []; -} diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs deleted file mode 100644 index 77aa22bf..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Reflection; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; -using Multifactor.Radius.Adapter.v2.Server; - -namespace Multifactor.Radius.Adapter.v2.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(AdapterServer)); - 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.v2.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs deleted file mode 100644 index 03fa63c5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs deleted file mode 100644 index c844d34c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs deleted file mode 100644 index 70acd784..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs deleted file mode 100644 index 250bb4af..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; - -namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; - -internal static class RadiusPacketFactory -{ - public static RadiusPacket AccessRequest(byte identifier = 0) - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, identifier); - var packet = new RadiusPacket(header); - return packet; - } - - public static RadiusPacket AccessChallenge(byte identifier = 0) - { - var header = RadiusPacketHeader.Create(PacketCode.AccessChallenge, identifier); - var packet = new RadiusPacket(header); - return packet; - } - - public static RadiusPacket AccessReject(byte identifier = 0) - { - var header = RadiusPacketHeader.Create(PacketCode.AccessReject, identifier); - var packet = new RadiusPacket(header); - return packet; - } - - public static RadiusPacket StatusServer(byte identifier = 0) - { - var header = RadiusPacketHeader.Create(PacketCode.StatusServer, identifier); - var packet = new RadiusPacket(header); - return packet; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs deleted file mode 100644 index db77aceb..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Fixtures/TestEnvironment.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/TestEnvironment.cs deleted file mode 100644 index 6ad7aa85..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/TestEnvironment.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Multifactor.Radius.Adapter.v2.EndToEndTests.csproj b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Multifactor.Radius.Adapter.v2.EndToEndTests.csproj deleted file mode 100644 index 912e7af0..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Multifactor.Radius.Adapter.v2.EndToEndTests.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - Linux - - - - - - - - - - - - - - - - - - - - - - Always - - - diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs deleted file mode 100644 index 180176ed..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.EndToEndTests; - -public class RadiusFixtures : IDisposable -{ - public IRadiusPacketService 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.v2.EndToEndTests/Tests/AccessChallengeTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs deleted file mode 100644 index e6afd3d9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Awaiting, state)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge1))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge2))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[] ?? []); - Assert.Equal(responseState, state); - - // Challenge step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge1); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(2, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - - // Challenge step 3 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 2); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge2); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(3, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Awaiting, state)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge1))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[] ?? []); - Assert.Equal(responseState, state); - - // Challenge step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge1); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(2, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessAccept, response.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", - - NpsServerEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.NpsServerEndpoint))!, - - AdapterClientEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.AdapterClientEndpoint))!, - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))! - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs deleted file mode 100644 index 01ea155e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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(); - AccessRequest? payload = null; - mfAPiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string a, AccessRequest x, ApiCredential y) => payload = x) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted} ); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - Assert.NotNull(payload); - Assert.False(string.IsNullOrWhiteSpace(payload.Email)); - Assert.False(string.IsNullOrWhiteSpace(payload.Name)); - Assert.False(string.IsNullOrWhiteSpace(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(); - AccessRequest? payload = null; - mfAPiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string a, AccessRequest x, ApiCredential y) => payload = x) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted} ); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, privacyMode: privacyMode); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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", - - NpsServerEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.NpsServerEndpoint))!, - - AdapterClientEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.AdapterClientEndpoint))!, - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - - PrivacyMode = privacyMode - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - PhoneAttributes = "mobile" - } - }, - - 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.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs deleted file mode 100644 index 25550b56..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs +++ /dev/null @@ -1,246 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData); - - await StartHostAsync(rootConfig, configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Fact] - public async Task BST002_ShouldAccept() - { - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData, true); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Fact] - public async Task BST003_ShouldAccept() - { - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Fact] - public async Task BST004_ShouldReject() - { - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData, false); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessReject, response.Code); - } - - [Fact] - public async Task BST005_ShouldAccept() - { - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData, true); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Fact] - public async Task BST006_ShouldAccept() - { - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var sensitiveData = E2ETestsUtils.GetConfigSensitiveData("root.ad.env"); - var rootConfig = CreateRootConfig(sensitiveData, false); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - private RadiusAdapterConfiguration CreateRootConfig(ConfigSensitiveData[] sensitiveData, bool? bypassSecondFactorWhenApiUnreachable = null) - { - var configName = "root"; - 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 - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - } - } - }; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs deleted file mode 100644 index 85f62730..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted }); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var userDn = new DistinguishedName(sensitiveData.GetConfigValue("root", "UserDn")!); - - // Password changing - ChangePassword( - userDn, - currentPassword: currentPassword, - newPassword: newPassword, - rootConfig); - - // Rollback - ChangePassword( - userDn, - 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.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted }); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var userDn = new DistinguishedName(sensitiveData.GetConfigValue("root", "UserDn")!); - - // Password changing - ChangePassword( - userDn, - currentPassword: currentPassword, - newPassword: newPassword, - rootConfig); - - // Rollback - ChangePassword( - userDn, - currentPassword: newPassword, - newPassword: currentPassword, - rootConfig); - } - - private void ChangePassword( - DistinguishedName userDn, - string currentPassword, - string newPassword, - RadiusAdapterConfiguration rootConfig) - { - SetAttributeForUserInCatalogAsync( - userDn, - rootConfig, - "pwdLastSet", - 0); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.ChangePasswordUserName); - accessRequest.AddAttributeValue("User-Password", currentPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[]); - Assert.True(Guid.TryParse(responseState, out Guid state)); - var stateString = state.ToString(); - - // New Password step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.ChangePasswordUserName); - accessRequest.AddAttributeValue("State", stateString); - accessRequest.AddAttributeValue("User-Password", newPassword); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - - // Repeat password step 3 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: _packetId++); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.ChangePasswordUserName); - accessRequest.AddAttributeValue("State",stateString); - accessRequest.AddAttributeValue("User-Password", newPassword); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(PacketCode.AccessAccept, response.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", - - 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))!, - }, - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = sensitiveData.GetConfigValue( - configName, - nameof(LdapServerConfiguration.UserName))!, - Password = sensitiveData.GetConfigValue( - configName, - nameof(LdapServerConfiguration.Password))!, - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs deleted file mode 100644 index 83db507b..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var serverSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, serverSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env", AccountType.Microsoft)] - [InlineData("radius-root-conf.env", AccountType.Microsoft)] - [InlineData("ad-root-conf.env", AccountType.Local)] - [InlineData("radius-root-conf.env", AccountType.Local)] - [InlineData("ad-root-conf.env", AccountType.Unknown)] - [InlineData("radius-root-conf.env", AccountType.Unknown)] - public async Task BST016_NotDomainAccount_ShouldAccept(string configName, AccountType accountType) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var mfAPiMock = new Mock(); - - mfAPiMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var serverSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, serverSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - accessRequest.AddAttributeValue("Acct-Authentic", (uint)accountType); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - [InlineData("radius-root-conf.env")] - public async Task BST017_ShouldReject(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var mfApiMock = new Mock(); - - mfApiMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfApiMock.Object); - }; - - var serverSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, serverSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", "BadPassword"); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(mfApiMock.Invocations); - Assert.Equal(PacketCode.AccessReject, response.Code); - } - - [Theory] - [InlineData("no-ldap-radius-conf.env")] - [InlineData("no-ldap-none-conf.env")] - public async Task FirstFactor_NoLdapServerSettings_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var mfAPiMock = new Mock(); - - mfAPiMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, new()); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData, LdapServersSection ldapServersSection) - { - 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", - - NpsServerEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.NpsServerEndpoint))!, - - AdapterClientEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.AdapterClientEndpoint))!, - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))! - }, - - LdapServers = ldapServersSection - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs deleted file mode 100644 index f2a3eccf..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, $"cn=Not,dc=Existed,dc=Group1;cn=Not,dc=Existed,dc=Group2;{sensitiveData.GetConfigValue("root", "AccessGroups")!}"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST012_NestedGroups_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, $"cn=Not,dc=Existed,dc=Group1;cn=Not,dc=Existed,dc=Group2;{sensitiveData.GetConfigValue("root", "NestedAccessGroups")!}"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group1;cn=Not,dc=Existed,dc=Group2"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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", - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - SecondFaGroups = activeDirectory2FaGroups, - LoadNestedGroups = true - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs deleted file mode 100644 index b4062034..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, $"cn=Not,dc=Existed,dc=Group;{ sensitiveData.GetConfigValue("root", "AccessGroups")! }"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST009_NestedGroup_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, $"cn=Not,dc=Existed,dc=Group;{ sensitiveData.GetConfigValue("root", "NestedAccessGroups")! }"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group1;cn=Not,dc=Existed,dc=Group2"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessReject, response.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", - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - AccessGroups = activeDirectoryGroups, - LoadNestedGroups = true - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs deleted file mode 100644 index 8f05357c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs +++ /dev/null @@ -1,476 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept, state)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var ldapServersSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, ldapServersSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 BST021_DomainUser_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 MultifactorResponse(AuthenticationStatus.Accept, state)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var ldapServersSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, ldapServersSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - //Should check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)AccountType.Domain); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - Assert.False(response.Attributes.ContainsKey("State")); - } - - [Theory] - [InlineData("none-root-conf.env", AccountType.Microsoft)] - [InlineData("none-root-conf.env", AccountType.Local)] - [InlineData("ad-root-conf.env", AccountType.Microsoft)] - [InlineData("ad-root-conf.env", AccountType.Local)] - [InlineData("radius-root-conf.env", AccountType.Microsoft)] - [InlineData("radius-root-conf.env", AccountType.Local)] - public async Task BST021_NotDomainUser_ShouldAccept(string configName, AccountType accountType) - { - var state = "BST021_ShouldAccept"; - - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var mfAPiMock = new Mock(); - - mfAPiMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept, state)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var ldapServersSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, ldapServersSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - //Should not check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)accountType); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Awaiting, state)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge1))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var ldapServersSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, ldapServersSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[]); - Assert.Equal(responseState, state); - - // Challenge step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge1); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(2, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Awaiting, state)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge1))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge2))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var ldapServersSection = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = - sensitiveData.GetConfigValue("root", nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, ldapServersSection); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[]); - Assert.Equal(responseState, state); - - // Challenge step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge1); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(2, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - - // Challenge step 3 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 2); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge2); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(3, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("no-ldap-radius-conf.env")] - [InlineData("no-ldap-none-conf.env")] - public async Task PreAuth_NoLdapServerSettings_ShouldAccept(string configName) - { - var state = "PreAuth_NoLdapServerSettings"; - 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 MultifactorResponse(AuthenticationStatus.Awaiting, state)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge1))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting)); - - mfAPiMock - .Setup(x => x.SendChallengeAsync(It.Is(r => r.Answer == challenge2))) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, new()); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - // AccessRequest step 1 - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 0); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - var attribute = response.Attributes["State"]; - Assert.NotNull(attribute.Values.FirstOrDefault()); - var attributeValue = attribute.Values.FirstOrDefault(); - var responseState = Encoding.UTF8.GetString(attributeValue as byte[]); - Assert.Equal(responseState, state); - - // Challenge step 2 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 1); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge1); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(2, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessChallenge, response.Code); - - // Challenge step 3 - accessRequest = CreateRadiusPacket(PacketCode.AccessRequest, identifier: 2); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("State", state); - accessRequest.AddAttributeValue("User-Password", challenge2); - - response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Equal(3, mfAPiMock.Invocations.Count); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - private RadiusAdapterConfiguration CreateRadiusConfiguration(ConfigSensitiveData[] sensitiveData, LdapServersSection ldapServersSection) - { - 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 = "any", - InvalidCredentialDelay = "3", - - NpsServerEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.NpsServerEndpoint))!, - - NpsServerTimeout = "00:00:10", - - AdapterClientEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.AdapterClientEndpoint))!, - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))! - }, - - LdapServers = ldapServersSection - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs deleted file mode 100644 index f85925f4..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(mfAPiMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(mfAPiMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - Assert.NotEmpty(response.Attributes); - Assert.True(response.Attributes.ContainsKey("Class")); - var classAttribute = response.Attributes["Class"]; - Assert.NotEmpty(classAttribute.Values); - } - - 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", - - NpsServerEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.NpsServerEndpoint))!, - - AdapterClientEndpoint = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.AdapterClientEndpoint))!, - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword - } - }, - - 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.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs deleted file mode 100644 index 957ef9d6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST014_DomainUser_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)AccountType.Domain); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env", AccountType.Microsoft)] - [InlineData("ad-root-conf.env", AccountType.Local)] - public async Task BST014_NotDomainUser_ShouldAccept(string configName, AccountType accountType) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should not check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)accountType); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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", - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - LoadNestedGroups = true, - SecondFaBypassGroups = activeDirectory2FaBypassGroup - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs deleted file mode 100644 index cce5c320..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST011_NestedGroup_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "NestedAccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST011_DomainUser_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)AccountType.Domain); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env", AccountType.Microsoft)] - [InlineData("ad-root-conf.env", AccountType.Local)] - public async Task BST011_NotDomainUser_ShouldAccept(string configName, AccountType accountType) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should not check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)accountType); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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", - - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))!, - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - SecondFaGroups = groups, - LoadNestedGroups = true - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs deleted file mode 100644 index ce3c0901..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "AccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - public async Task BST007_NestedGroup_ShouldAccept(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, sensitiveData.GetConfigValue("root", "NestedAccessGroups")!); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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 MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessReject, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env")] - [InlineData("none-root-conf.env")] - public async Task BST008_DomainUser_ShouldReject(string configName) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)AccountType.Domain); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Empty(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessReject, response.Code); - } - - [Theory] - [InlineData("ad-root-conf.env", AccountType.Microsoft)] - [InlineData("ad-root-conf.env", AccountType.Local)] - [InlineData("none-root-conf.env", AccountType.Microsoft)] - [InlineData("none-root-conf.env", AccountType.Local)] - public async Task BST008_NotDomainUser_ShouldAccept(string configName, AccountType accountType) - { - var sensitiveData = - E2ETestsUtils.GetConfigSensitiveData(configName); - - var secondFactorMock = new Mock(); - - secondFactorMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - - var hostConfiguration = (HostApplicationBuilder builder) => - { - builder.Services.ReplaceService(secondFactorMock.Object); - }; - - var rootConfig = CreateRadiusConfiguration(sensitiveData, "cn=Not,dc=Existed,dc=Group"); - - await StartHostAsync( - rootConfig, - configure: hostConfiguration); - - var accessRequest = CreateRadiusPacket(PacketCode.AccessRequest); - accessRequest.AddAttributeValue("NAS-Identifier", RadiusAdapterConstants.DefaultNasIdentifier); - accessRequest.AddAttributeValue("User-Name", RadiusAdapterConstants.BindUserName); - accessRequest.AddAttributeValue("User-Password", RadiusAdapterConstants.BindUserPassword); - //Should not check groups - accessRequest.AddAttributeValue("Acct-Authentic", (uint)accountType); - - var response = SendPacketAsync(accessRequest); - - Assert.NotNull(response); - Assert.Single(secondFactorMock.Invocations); - Assert.Equal(PacketCode.AccessAccept, response.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", - FirstFactorAuthenticationSource = sensitiveData.GetConfigValue( - configName, - nameof(AppSettingsSection.FirstFactorAuthenticationSource))! - }, - - LdapServers = new LdapServersSection() - { - LdapServer = new LdapServerConfiguration() - { - ConnectionString = sensitiveData.GetConfigValue(configName, nameof(LdapServerConfiguration.ConnectionString))!, - UserName = RadiusAdapterConstants.AdminUserName, - Password = RadiusAdapterConstants.AdminUserPassword, - AccessGroups = activeDirectoryGroup, - LoadNestedGroups = true - } - } - }; - - return rootConfig; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpData.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpData.cs deleted file mode 100644 index 5b82f46d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpData.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.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.v2.EndToEndTests/Udp/UdpSocket.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpSocket.cs deleted file mode 100644 index e74264b0..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpSocket.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.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.v2/Core/Ldap/CustomLdapConnectionFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs similarity index 56% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/CustomLdapConnectionFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs index 264f5d9c..9f72b581 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/CustomLdapConnectionFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Logging.Abstractions; +using System.Runtime.InteropServices; using Multifactor.Core.Ldap.Connection; using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap; public class CustomLdapConnectionFactory : ILdapConnectionFactory { @@ -12,14 +12,11 @@ public CustomLdapConnectionFactory() { _factory = LdapConnectionFactory.Create(); } - - public CustomLdapConnectionFactory(IEnumerable ldapConnectionFactories) - { - _factory = new LdapConnectionFactory (NullLogger.Instance, ldapConnectionFactories); - } public ILdapConnection CreateConnection(LdapConnectionOptions ldapConnectionOptions) { return new LdapConnection(_factory.CreateConnection(ldapConnectionOptions)); } + + public OSPlatform TargetPlatform { get; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs new file mode 100644 index 00000000..b6325157 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -0,0 +1,167 @@ +using System.DirectoryServices.Protocols; +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap; +using Multifactor.Core.Ldap.Connection; +using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; +using Multifactor.Core.Ldap.Extensions; +using Multifactor.Core.Ldap.LdapGroup.Load; +using Multifactor.Core.Ldap.LdapGroup.Membership; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap; + +public sealed class LdapAdapter : ILdapAdapter +{ + private readonly ILdapConnectionFactory _connectionFactory; + private readonly LdapSchemaLoader _schemaLoader; + private readonly IMembershipCheckerFactory _ldapMembershipCheckerFactory; + private readonly ILdapGroupLoaderFactory _ldapGroupLoaderFactory; + private readonly ILogger _logger; + + public LdapAdapter( + ILdapConnectionFactory connectionFactory, + LdapSchemaLoader schemaLoader, + ILogger logger, IMembershipCheckerFactory ldapMembershipCheckerFactory, ILdapGroupLoaderFactory ldapGroupLoaderFactory) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _schemaLoader = schemaLoader ?? throw new ArgumentNullException(nameof(schemaLoader)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _ldapMembershipCheckerFactory = ldapMembershipCheckerFactory; + _ldapGroupLoaderFactory = ldapGroupLoaderFactory; + } + + public IReadOnlyList LoadUserGroups(LoadUserGroupRequest request) + { + using var connection = CreateConnection(request.ConnectionData); + var groupLoader = _ldapGroupLoaderFactory.GetGroupLoader(request.LdapSchema, connection, request.SearchBase ?? request.LdapSchema.NamingContext); + var groupDns = groupLoader.GetGroups(request.UserDN, pageSize: 20); + return groupDns.Take(request.Limit).Select(x => x.Components.Deepest.Value).ToList(); + } + + #region FindUserProfile + public ILdapProfile? FindUserProfile(FindUserRequest request) + { + _logger.LogInformation("Try to find '{userIdentity}' profile at '{domain}'.", request.UserIdentity.Identity, request.SearchBase.StringRepresentation); + using var connection = CreateConnection(request.ConnectionData); + var identityToSearch = request.UserIdentity; + if (request.UserIdentity.Format == UserIdentityFormat.NetBiosName) + { + var index = request.UserIdentity.Identity.IndexOf('\\'); + if (index <= 0) + throw new ArgumentException($"Invalid NetBIOS identity: {request.UserIdentity.Identity}"); + var userName = request.UserIdentity.Identity[(index + 1)..]; + identityToSearch = new UserIdentity(userName); + } + var filter = GetFilter(identityToSearch, request.LdapSchema); + _logger.LogDebug("Search base = '{searchBase}'. Filter for search = '{filter}'", request.SearchBase.StringRepresentation, filter); + var result = connection.Find(request.SearchBase, filter, SearchScope.Subtree, attributes: request.AttributeNames ?? []); + var entry = result.FirstOrDefault(); + return entry is null ? null : new LdapProfile(entry, request.LdapSchema); + } + + private string GetFilter(UserIdentity identity, ILdapSchema schema) + { + var identityAttribute = GetIdentityAttribute(identity, schema); + var objectClass = schema.ObjectClass; + var classValue = schema.UserObjectClass; + + return $"(&({objectClass}={classValue})({identityAttribute}={identity.Identity}))"; + } + + private static string GetIdentityAttribute(UserIdentity identity, ILdapSchema schema) => identity.Format switch + { + UserIdentityFormat.UserPrincipalName => "userPrincipalName", + UserIdentityFormat.DistinguishedName => schema.Dn, + UserIdentityFormat.SamAccountName => schema.Uid, + _ => throw new NotSupportedException("Unsupported user identity format") + }; + #endregion + + public ILdapSchema? LoadSchema(LdapConnectionData request) + { + var options = new LdapConnectionOptions( + new LdapConnectionString(request.ConnectionString, true), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds) + ); + return _schemaLoader.Load(options); + } + + public bool CheckConnection(LdapConnectionData request) + { + using var connection = CreateConnection(request); + return true; //true of exception + } + + #region IsMemberOf + public bool IsMemberOf(MembershipRequest request) + { + ArgumentNullException.ThrowIfNull(request); + if(request.TargetGroups == null || request.TargetGroups.Length == 0) + throw new InvalidOperationException(); + using var connection = CreateConnection(request.ConnectionData); + + return request.NestedGroupsBaseDns.Length > 0 + ? request.NestedGroupsBaseDns + .Select(groupBaseDn => IsMemberOf(request, connection, groupBaseDn)) + .Any(isMemberOf => isMemberOf) + : IsMemberOf(request, connection); + } + + private bool IsMemberOf(MembershipRequest request, ILdapConnection connection, DistinguishedName? searchBase = null) + { + var membershipChecker = _ldapMembershipCheckerFactory.GetMembershipChecker(request.LdapSchema, connection, searchBase ?? request.LdapSchema.NamingContext); + return membershipChecker.IsMemberOf(request.DistinguishedName, request.TargetGroups.ToArray()); + } + #endregion + + #region ChangeUserPassword + public bool ChangeUserPassword(ChangeUserPasswordRequest request) + { + ArgumentNullException.ThrowIfNull(request, nameof(request)); + + using var connection = CreateConnection(request.ConnectionData); + var changePasswordRequest = BuildPasswordChangeRequest(request.LdapSchema, request.DistinguishedName, request.NewPassword); + var response = connection.SendRequest(changePasswordRequest); + return response.ResultCode == ResultCode.Success; + } + + private static ModifyRequest BuildPasswordChangeRequest(ILdapSchema ldapSchema, DistinguishedName userDn, string newPassword) + { + var attributeName = ldapSchema.LdapServerImplementation == LdapImplementation.ActiveDirectory + ? "unicodePwd" + : "userpassword"; + + var newPasswordAttribute = new DirectoryAttributeModification + { + Name = attributeName, + Operation = DirectoryAttributeOperation.Replace + }; + if (ldapSchema.LdapServerImplementation == LdapImplementation.ActiveDirectory) + newPasswordAttribute.Add(Encoding.Unicode.GetBytes($"\"{newPassword}\"")); + else + newPasswordAttribute.Add(newPassword); + + return new ModifyRequest(userDn.StringRepresentation, newPasswordAttribute); + } + #endregion + + private ILdapConnection CreateConnection(LdapConnectionData data) + { + var options = new LdapConnectionOptions(new LdapConnectionString(data.ConnectionString, true, false), + AuthType.Basic, + data.UserName, + data.Password, + TimeSpan.FromSeconds(data.BindTimeoutInSeconds)); + return _connectionFactory.CreateConnection(options); + } + +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnection.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapConnection.cs similarity index 80% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnection.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapConnection.cs index 9285c38b..8979c1d0 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnection.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapConnection.cs @@ -1,17 +1,18 @@ using System.Collections.ObjectModel; using System.DirectoryServices.Protocols; using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Connection; using Multifactor.Core.Ldap.Entry; using Multifactor.Core.Ldap.Extensions; using Multifactor.Core.Ldap.Name; -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap; public class LdapConnection : ILdapConnection { - private readonly Multifactor.Core.Ldap.Connection.ILdapConnection _ldapConnection; + private readonly ILdapConnection _ldapConnection; - public LdapConnection(Multifactor.Core.Ldap.Connection.ILdapConnection connection) + public LdapConnection(ILdapConnection connection) { ArgumentNullException.ThrowIfNull(connection); _ldapConnection = connection; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs new file mode 100644 index 00000000..6ca8551b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs @@ -0,0 +1,45 @@ +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; + +public class ActivityContext +{ + private static readonly AsyncLocal _value = new(); + + /// + /// Current context activity id. + /// + public string ActivityId { get; private set; } + + /// + /// Returns current ActivityContext or creates new if null. + /// + public static ActivityContext Current + { + get => _value.Value ??= new ActivityContext(); + set => _value.Value = value; + } + + private ActivityContext() + { + ActivityId = Guid.NewGuid().ToString(); + Current = this; + } + + private ActivityContext(string activityId) + { + ActivityId = activityId; + Current = this; + } + + /// + /// Creates and sets current ActivityContext then returns it. + /// + /// Specified activity id. + /// Current ActivityContext. + public static ActivityContext Create(string activityId) => new(activityId); + + /// + /// Sets activity id to the current ActivityContext. + /// + /// New activity id. + public void SetActivityId(string activityId) => ActivityId = activityId; +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs new file mode 100644 index 00000000..707b56fe --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs @@ -0,0 +1,83 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http +{ + /// + /// Represents value (parameter) for a BASIC authentication header. + /// {username}:{password} + /// + public class BasicAuthHeaderValue + { + private readonly string _username; + private readonly string _password; + private readonly string _base64; + + public BasicAuthHeaderValue(string username, string password) + { + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException($"'{nameof(username)}' cannot be null or whitespace.", nameof(username)); + } + + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException($"'{nameof(password)}' cannot be null or whitespace.", nameof(password)); + } + _username = username; + _password = password; + _base64 = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")); + } + + /// + /// Returns BASE64 header value representation. + /// + /// + public string GetBase64() => _base64; + + public override string ToString() => $"{_username}:{_password}"; + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (GetType() != obj.GetType()) + { + return false; + } + + var val = (BasicAuthHeaderValue)obj; + return val._base64 == _base64; + } + + public override int GetHashCode() + { + unchecked + { + return 17 * _base64.GetHashCode(); + } + } + + public static bool operator ==(BasicAuthHeaderValue a, BasicAuthHeaderValue b) + { + if (a is null && b is null) + { + return true; + } + + if (a is null || b is null) + { + return false; + } + + return a.Equals(b); + } + + public static bool operator !=(BasicAuthHeaderValue a, BasicAuthHeaderValue b) + { + return !(a == b); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs new file mode 100644 index 00000000..37d384e3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs @@ -0,0 +1,23 @@ +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; + +public class MfTraceIdHeaderSetter : DelegatingHandler +{ + private const string _key = "mf-trace-id"; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var trace = $"rds-{ActivityContext.Current.ActivityId}"; + if (!request.Headers.Contains(_key)) + { + request.Headers.Add(_key, trace); + } + + var resp = await base.SendAsync(request, cancellationToken); + if (!resp.Headers.Contains(_key)) + { + resp.Headers.Add(_key, trace); + } + + return resp; + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs new file mode 100644 index 00000000..80139201 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs @@ -0,0 +1,55 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; + +public interface IEndpointSelector +{ + Task GetNextEndpointAsync(); +} + +public class RoundRobinEndpointSelector : IEndpointSelector +{ + private readonly IReadOnlyList _endpoints; + private readonly ConcurrentDictionary _failedEndpoints; + private int _currentIndex = -1; + private readonly object _lock = new(); + private readonly ILogger _logger; + + public RoundRobinEndpointSelector(ServiceConfiguration configuration, + ILogger logger) + { + _endpoints = configuration.RootConfiguration.MultifactorApiUrls; + _failedEndpoints = new ConcurrentDictionary(); + _logger = logger; + } + + public async Task GetNextEndpointAsync() + { + return await GetNextHealthyEndpointAsync(); + } + + private Task GetNextHealthyEndpointAsync() + { + if (_endpoints.Count == 0) + throw new InvalidOperationException("No endpoints configured"); + + lock (_lock) + { + foreach (var _ in _endpoints) + { + _currentIndex = (_currentIndex + 1) % _endpoints.Count; + var endpoint = _endpoints[_currentIndex]; + + if (_failedEndpoints.ContainsKey(endpoint)) continue; + _logger.LogDebug("Selected endpoint: {Endpoint}", endpoint); + return Task.FromResult(endpoint); + } + _failedEndpoints.Clear(); + _currentIndex = 0; + _logger.LogWarning("All endpoints failed, resetting to first"); + return Task.FromResult(_endpoints[0]); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/WebProxyFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/WebProxyFactory.cs similarity index 77% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/WebProxyFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/WebProxyFactory.cs index 9d4fac11..dca42169 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/WebProxyFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/WebProxyFactory.cs @@ -1,18 +1,12 @@ using System.Net; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; public static class WebProxyFactory { public static bool TryCreateWebProxy(string proxyAddress, out WebProxy? proxy) { - if (string.IsNullOrWhiteSpace(proxyAddress)) - { - proxy = null; - return false; - } - - if (!TryParseUri(proxyAddress, out var proxyUri)) + if (string.IsNullOrWhiteSpace(proxyAddress) || !TryParseUri(proxyAddress, out var proxyUri)) { proxy = null; return false; @@ -44,7 +38,7 @@ private static void SetProxyCredentials(WebProxy proxy, Uri proxyUri) if (string.IsNullOrWhiteSpace(proxyUri.UserInfo)) return; - var credentials = proxyUri.UserInfo.Split(new[] { ':' }, 2); + var credentials = proxyUri.UserInfo.Split([':'], 2); proxy.Credentials = new NetworkCredential(credentials[0], credentials[1]); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs new file mode 100644 index 00000000..cc231d77 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs @@ -0,0 +1,41 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; + +public class AccessRequestDto +{ + public string? Identity { get; set; } + public string? Name { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? PassCode { get; set; } + public string? CallingStationId { get; set; } + public string? CalledStationId { get; set; } + public Capabilities? Capabilities { get; set; } + public GroupPolicyPreset? GroupPolicyPreset { get; set; } + + public static AccessRequestDto FromQuery(AccessRequestQuery query) + { + return new AccessRequestDto + { + Identity = query.Identity, + Name = query.Name, + Email = query.Email, + Phone = query.Phone, + PassCode = query.PassCode, + CalledStationId = query.CalledStationId, + CallingStationId = query.CallingStationId, + Capabilities = new Capabilities{ InlineEnroll = true }, + GroupPolicyPreset = new GroupPolicyPreset{ SignUpGroups = query.SignUpGroups } + }; + } +} + +public class Capabilities +{ + public bool InlineEnroll { get; set; } +} +public class GroupPolicyPreset +{ + public string SignUpGroups { get; set; } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs new file mode 100644 index 00000000..3102fa5e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs @@ -0,0 +1,20 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; + +public class ChallengeRequestDto +{ + public string Identity { get; set; } = string.Empty; + public string Challenge { get; set; } = string.Empty; + public string RequestId { get; set; } = string.Empty; + + public static ChallengeRequestDto FromQuery(ChallengeRequestQuery query) + { + return new ChallengeRequestDto + { + Identity = query.Identity, + Challenge = query.Challenge, + RequestId = query.RequestId + }; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/MultifactorApiResponse.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/MultifactorApiResponse.cs new file mode 100644 index 00000000..d0481a9f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/MultifactorApiResponse.cs @@ -0,0 +1,7 @@ +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; + +public class MultiFactorApiResponse +{ + public bool Success { get; set; } + public T Model { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs new file mode 100644 index 00000000..8b6f856c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs @@ -0,0 +1,186 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor; + +public class MultifactorApi : IMultifactorApi +{ + private const string ClientName = "multifactor-api"; + private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + + public MultifactorApi( + IHttpClientFactory clientFactory, + ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task CreateAccessRequest( + AccessRequestQuery query, + MultifactorAuthData authData, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query, nameof(query)); + + var dto = AccessRequestDto.FromQuery(query); + return await SendRequestAsync( + endpoint: "access/requests/ra", + data: dto, + authData: authData, + cancellationToken: cancellationToken); + } + + public async Task SendChallengeAsync( + ChallengeRequestQuery query, + MultifactorAuthData authData, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query, nameof(query)); + + var dto = ChallengeRequestDto.FromQuery(query); + return await SendRequestAsync( + endpoint: "access/requests/ra/challenge", + data: dto, + authData: authData, + cancellationToken: cancellationToken); + } + + private async Task SendRequestAsync( + string endpoint, + TRequest data, + MultifactorAuthData authData, + CancellationToken cancellationToken) + where TResponse : class, new() + { + using var client = CreateAuthenticatedClient(authData); + + try + { + var response = await client.PostAsJsonAsync( + endpoint, + data, + _jsonOptions, + cancellationToken); + + return await ProcessResponseAsync(response, endpoint, cancellationToken); + } + catch (HttpRequestException ex) + { + return ProcessHttpRequestException(ex, client.BaseAddress?.OriginalString); + } + catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) + { + _logger.LogWarning("Multifactor API timeout expired for endpoint: {Endpoint}", endpoint); + return CreateDeniedResponse("Request timeout"); + } + catch (OperationCanceledException) + { + _logger.LogWarning("Multifactor API request was cancelled for endpoint: {Endpoint}", endpoint); + throw; + } + catch (Exception ex) + { + throw new MultifactorApiUnreachableException( + $"Multifactor API host unreachable: {client.BaseAddress?.OriginalString}. " + + $"Endpoint: {endpoint}. Reason: {ex.Message}", + ex); + } + } + + private HttpClient CreateAuthenticatedClient(MultifactorAuthData authData) + { + var client = _clientFactory.CreateClient(ClientName); + var authHeader = new BasicAuthHeaderValue(authData.ApiKey, authData.ApiSecret); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", + authHeader.GetBase64()); + + return client; + } + + private async Task ProcessResponseAsync( + HttpResponseMessage response, + string endpoint, + CancellationToken cancellationToken) + where TResponse : class, new() + { + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogError( + "Multifactor API request failed. Endpoint: {Endpoint}, Status: {StatusCode}, Error: {Error}", + endpoint, response.StatusCode, errorContent); + + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + return CreateDeniedResponse("Too Many Requests"); + } + + response.EnsureSuccessStatusCode(); + } + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var apiResponse = JsonSerializer.Deserialize>( + content, + _jsonOptions); + + if (apiResponse is null) + { + _logger.LogWarning("Empty response from Multifactor API for endpoint: {Endpoint}", endpoint); + return CreateDeniedResponse("Empty response"); + } + + if (!apiResponse.Success) + { + _logger.LogWarning( + "Unsuccessful response from Multifactor API. Endpoint: {Endpoint}, Response: {@Response}", + endpoint, apiResponse); + } + + return apiResponse.Model ?? CreateDeniedResponse("Response model is null"); + } + + private static TResponse CreateDeniedResponse(string? message = null) + where TResponse : class, new() + { + if (typeof(TResponse) == typeof(AccessRequestResponse)) + { + return (TResponse)(object)new AccessRequestResponse + { + Status = RequestStatus.Denied, + ReplyMessage = message + }; + } + + return new TResponse(); + } + + private TResponse ProcessHttpRequestException( + HttpRequestException ex, + string? url) + where TResponse : class, new() + { + if (ex.StatusCode != HttpStatusCode.TooManyRequests) + { + throw new MultifactorApiUnreachableException( + $"Multifactor API host unreachable: {url}. Reason: {ex.Message}", + ex); + } + + _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); + return CreateDeniedResponse("Too Many Requests"); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/UdpPacketHandler.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs similarity index 71% rename from src/Multifactor.Radius.Adapter.v2/Server/UdpPacketHandler.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs index e7896337..6225b4de 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/UdpPacketHandler.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs @@ -2,31 +2,29 @@ using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.Server; - -public class UdpPacketHandler : IUdpPacketHandler +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.PacketHandler; + +public class RadiusUdpAdapter : IRadiusUdpAdapter { - private readonly ILogger _logger; - private readonly IServiceConfiguration _serviceConfiguration; + private readonly ILogger _logger; + private readonly ServiceConfiguration _serviceConfiguration; private readonly IRadiusPacketService _radiusPacketService; private readonly ICacheService _cache; private readonly IRadiusPacketProcessor _radiusPacketProcessor; - public UdpPacketHandler( - IServiceConfiguration serviceConfiguration, + public RadiusUdpAdapter( + ServiceConfiguration serviceConfiguration, IRadiusPacketService packetService, ICacheService cache, IRadiusPacketProcessor radiusPacketProcessor, - ILogger logger) + ILogger logger) { _serviceConfiguration = serviceConfiguration; _radiusPacketService = packetService; @@ -35,7 +33,7 @@ public UdpPacketHandler( _cache = cache; } - public async Task HandleUdpPacket(UdpReceiveResult udpPacket) + public async Task Handle(UdpReceiveResult udpPacket) { IPEndPoint? proxyEndpoint = null; var remoteEndpoint = udpPacket.RemoteEndPoint; @@ -55,7 +53,7 @@ public async Task HandleUdpPacket(UdpReceiveResult udpPacket) return; } - var requestPacket = _radiusPacketService.Parse(payload, new SharedSecret(clientConfiguration.RadiusSharedSecret)); + var requestPacket = _radiusPacketService.ParsePacket(payload, new SharedSecret(clientConfiguration.RadiusSharedSecret)); requestPacket.ProxyEndpoint = proxyEndpoint; requestPacket.RemoteEndpoint = remoteEndpoint; @@ -68,7 +66,7 @@ public async Task HandleUdpPacket(UdpReceiveResult udpPacket) await _radiusPacketProcessor.ProcessPacketAsync(requestPacket, clientConfiguration); } - private bool IsProxyProtocol(byte[] payload, out IPEndPoint sourceEndpoint, out byte[] requestWithoutProxyHeader) + private static bool IsProxyProtocol(byte[] payload, out IPEndPoint sourceEndpoint, out byte[] requestWithoutProxyHeader) { //https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt @@ -102,13 +100,13 @@ private bool IsProxyProtocol(byte[] payload, out IPEndPoint sourceEndpoint, out { IClientConfiguration? clientConfiguration = null; if (_radiusPacketService.TryGetNasIdentifier(udpPacket.Buffer, out var nasIdentifier)) - clientConfiguration = _serviceConfiguration.GetClient(nasIdentifier); - clientConfiguration ??= _serviceConfiguration.GetClient(udpPacket.RemoteEndPoint.Address); + clientConfiguration = _serviceConfiguration.GetClientConfiguration(nasIdentifier); + clientConfiguration ??= _serviceConfiguration.GetClientConfiguration(udpPacket.RemoteEndPoint.Address); return clientConfiguration; } - private bool IsRetransmission(IRadiusPacket requestPacket) + private bool IsRetransmission(RadiusPacket requestPacket) { var packetKey = CreateUniquePacketKey(requestPacket); if (_cache.TryGetValue(packetKey, out _)) @@ -119,9 +117,9 @@ private bool IsRetransmission(IRadiusPacket requestPacket) return false; } - private string CreateUniquePacketKey(IRadiusPacket requestPacket) + private static string CreateUniquePacketKey(RadiusPacket requestPacket) { - var base64Authenticator = requestPacket.Authenticator.Value.Base64(); + var base64Authenticator = requestPacket.Authenticator.Value.ToBase64(); return $"{requestPacket.Code:d}:{requestPacket.Identifier}:{requestPacket.RemoteEndpoint}:{requestPacket.UserName}:{base64Authenticator}"; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs new file mode 100644 index 00000000..5c1b4477 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.LangFeatures; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Udp; + +public sealed class CustomUdpClient : IUdpClient +{ + private readonly UdpClient _udpClient; + private readonly ILogger _logger; + + public CustomUdpClient( + IPEndPoint endPoint, + ILogger logger) + { + Throw.IfNull(endPoint, nameof(endPoint)); + + _logger = logger; + _udpClient = new UdpClient(endPoint); + + _logger.LogInformation("UDP client initialized on {Endpoint}", endPoint); + } + + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + => await _udpClient.ReceiveAsync(cancellationToken); + + + public async Task SendAsync( + byte[] datagram, + int bytesCount, + IPEndPoint endPoint) + { + ArgumentNullException.ThrowIfNull(datagram); + + if (bytesCount <= 0 || bytesCount > datagram.Length) + throw new ArgumentOutOfRangeException(nameof(bytesCount)); + + if (bytesCount > 4096) + { + _logger.LogWarning("Attempted to send oversized RADIUS packet: {Size} bytes", bytesCount); + throw new ArgumentException($"RADIUS packet too large: {bytesCount} bytes"); + } + + try + { + await _udpClient.SendAsync(datagram, bytesCount, endPoint); + return bytesCount; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.MessageSize) + { + _logger.LogWarning(ex, "Packet too large for MTU to {Endpoint}", endPoint); + throw; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.HostUnreachable) + { + _logger.LogWarning(ex, "Host unreachable: {Endpoint}", endPoint); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "UDP send error to {Endpoint}", endPoint); + throw; + } + } + + public void Dispose() + { + try + { + _udpClient.Close(); + _udpClient.Dispose(); + _logger.LogDebug("UDP client disposed"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error disposing UDP client"); + } + } +} diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs similarity index 52% rename from src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClient.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs index 5fc49842..1afb71d3 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Cache.AuthenticatedClientCache; public class AuthenticatedClient { @@ -7,20 +7,12 @@ public class AuthenticatedClient public string Id { get; } public TimeSpan Elapsed => DateTime.Now - _authenticatedAt; - public AuthenticatedClient(string id, DateTime authenticatedAt) - { - ArgumentException.ThrowIfNullOrWhiteSpace(id); - - Id = id; - _authenticatedAt = authenticatedAt; - } - - public static AuthenticatedClient Create(params string?[] components) + public AuthenticatedClient(params string?[] components) { ArgumentNullException.ThrowIfNull(components); if (components.Length == 0) throw new ArgumentException(nameof(components)); - - return new AuthenticatedClient(ParseId(components), DateTime.Now); + Id = ParseId(components); + _authenticatedAt = DateTime.Now; } public static string ParseId(params string?[] components) => string.Join('-', components.Where(x => !string.IsNullOrWhiteSpace(x))); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs new file mode 100644 index 00000000..4a54f3a6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Cache; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Cache.AuthenticatedClientCache; + +public class AuthenticatedClientCache : IAuthenticatedClientCache +{ + private readonly IMemoryCache _memoryCache; + private readonly ILogger _logger; + + public AuthenticatedClientCache(IMemoryCache memoryCache, ILogger logger) + { + _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public bool TryHitCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime) + { + ArgumentException.ThrowIfNullOrWhiteSpace(clientName); + + if (lifetime == TimeSpan.Zero) + return false; + + if (string.IsNullOrWhiteSpace(callingStationId)) + { + _logger.LogError("Remote host parameter miss for user {userName:l}", userName); + return false; + } + + var id = AuthenticatedClient.ParseId(callingStationId, clientName, userName); + + if (!_memoryCache.TryGetValue(id, out var cachedValue)) + return false; + + if (cachedValue is AuthenticatedClient authenticatedClient) + { + _logger.LogDebug($"User {userName} with calling-station-id {callingStationId} authenticated {authenticatedClient.Elapsed:hh\\:mm\\:ss} ago. Authentication session period: {lifetime}"); + return true; + } + + return false; + } + + public void SetCache(string? callingStationId, string? userName, string clientName, TimeSpan lifetime) + { + ArgumentException.ThrowIfNullOrWhiteSpace(clientName); + + if (lifetime == TimeSpan.Zero || string.IsNullOrWhiteSpace(callingStationId)) + return; + + var id = AuthenticatedClient.ParseId(callingStationId, clientName, userName); + + if (!_memoryCache.TryGetValue(id, out _)) + { + var client = new AuthenticatedClient([callingStationId, clientName, userName]); + var cacheOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = lifetime + }; + + _memoryCache.Set(id, client, cacheOptions); + + var expirationDate = DateTimeOffset.Now.Add(lifetime); + _logger.LogDebug("Authentication for user '{userName}' is saved in cache till '{expiration}' with key '{key}'", + userName, expirationDate.ToString("O"), id); + } + else + { + _logger.LogDebug("Cache entry for user '{userName}' with key '{key}' already exists", userName, id); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Cache/CacheService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/CacheService.cs similarity index 75% rename from src/Multifactor.Radius.Adapter.v2/Services/Cache/CacheService.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/CacheService.cs index b15fdb01..1a26d5c2 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Cache/CacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/CacheService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Caching.Memory; +using Multifactor.Radius.Adapter.v2.Application.Cache; -namespace Multifactor.Radius.Adapter.v2.Services.Cache; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Cache; public class CacheService : ICacheService { @@ -19,14 +20,6 @@ public void Set(string key, T value, DateTimeOffset expirationDate) _cache.Set(key, value, expirationDate); } - public void Set(string key, T value) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentNullException(nameof(key)); - - _cache.Set(key, value); - } - public bool TryGetValue(string key, out T? value) { if (string.IsNullOrWhiteSpace(key)) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs new file mode 100644 index 00000000..ec18ab8b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -0,0 +1,74 @@ +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; + +/// +/// The Radius adapter configuration is invalid. +/// +internal class InvalidConfigurationException : Exception +{ + public InvalidConfigurationException(string message) + : base($"Configuration error: {message}") { } + public InvalidConfigurationException(string message, string fileName) + : base($"Configuration error: {message}. Configuration file name: {fileName}") { } + + public InvalidConfigurationException(string message, Exception inner) + : base($"Configuration error: {message}", inner) { } + + public static InvalidConfigurationException For(Expression> propertySelector, + string formattedMessage, + params object[] args) + { + if (propertySelector is null) + { + throw new ArgumentNullException(nameof(propertySelector)); + } + + if (string.IsNullOrWhiteSpace(formattedMessage)) + { + throw new ArgumentException($"'{nameof(formattedMessage)}' cannot be null or whitespace.", nameof(formattedMessage)); + } + + var propertyName = Property(propertySelector); + + formattedMessage = formattedMessage.Replace("{prop}", propertyName); + formattedMessage = string.Format(formattedMessage, args); + + return new InvalidConfigurationException(formattedMessage); + } + + private static string Property(Expression> propertySelector) + { + if (propertySelector is null) + { + throw new ArgumentNullException(nameof(propertySelector)); + } + + if (propertySelector.Body is not MemberExpression expression) + { + throw new InvalidOperationException("Only the class property should be selected"); + } + + if (expression.Member is not PropertyInfo property) + { + throw new InvalidOperationException("Only the class property should be selected"); + } + + var attribute = property.GetCustomAttribute(); + if (attribute == null) + { + return property.Name; + } + + var description = attribute.Description; + if (string.IsNullOrWhiteSpace(description)) + { + return property.Name; + } + + return description; + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs new file mode 100644 index 00000000..70445a26 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -0,0 +1,205 @@ +using System.Net; +using System.Reflection; +using System.Text.RegularExpressions; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; + +public class ConfigurationLoader : IConfigurationLoader +{ + private readonly IRadiusDictionary _dictionary; + + public ConfigurationLoader(IRadiusDictionary dictionary) + { + _dictionary = dictionary; + } + + public ServiceConfiguration Load() + { + StartupLogger.Information("===== Multifactor RADIUS Adapter ====="); + StartupLogger.Information("Application initialization started"); + StartupLogger.Information(_dictionary.GetInfo()); + var rootConfigPath = GetRootConfigPath(); + var rootConfig = LoadRootConfiguration(rootConfigPath); + var clients = LoadClientConfigurations(rootConfigPath); + + return new ServiceConfiguration + { + RootConfiguration = rootConfig, + ClientsConfigurations = clients, + SingleClientMode = clients.Count == 1 + }; + } + + private static string GetRootConfigPath() + { + var assemblyLocation = Assembly.GetEntryAssembly()?.Location; + return $"{assemblyLocation}.config"; + } + + private RootConfiguration LoadRootConfiguration(string configPath) + { + if (!File.Exists(configPath)) + throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); + var fileName = Path.GetFileNameWithoutExtension(configPath); + StartupLogger.Information($"Loading root configuration from '{fileName}'"); + var config = ReadConfiguration(configPath); + return RootConfiguration.FromConfiguration(config); + } + + private List LoadClientConfigurations(string rootConfigPath) + { + var clientsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "clients"); + + if (!Directory.Exists(clientsPath)) + { + var fileName = Path.GetFileNameWithoutExtension(rootConfigPath); + StartupLogger.Information($"Loading client configuration from '{fileName}'"); + var clientConfig = ParseClientConfiguration(rootConfigPath); + return [clientConfig]; + } + + var clientConfigFiles = Directory.GetFiles(clientsPath, "*.config"); + + if (clientConfigFiles.Length == 0) + { + var fileName = Path.GetFileNameWithoutExtension(rootConfigPath); + StartupLogger.Information($"Loading client configuration from '{fileName}'"); + var clientConfig = ParseClientConfiguration(rootConfigPath); + return [clientConfig]; + } + + return clientConfigFiles + .Select(ParseClientConfiguration) + .ToList(); + } + + private ClientConfiguration ParseClientConfiguration(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + StartupLogger.Information($"Loading client configuration from '{fileName}'"); + var prefix = GetConfigPrefix(filePath); + var config = ReadConfiguration(filePath, prefix); + + var clientConfig = ClientConfiguration.FromConfiguration(config); + clientConfig.ReplyAttributes = ParseReplyAttributes(config.RadiusReply); + clientConfig.LdapServers = config.LdapServers.Select(conf => LdapServerConfiguration.FromConfiguration(conf, clientConfig.Name)).ToList(); + + return clientConfig; + } + + private static AdapterConfiguration ReadConfiguration(string filePath, string prefix = null) + { + if (!File.Exists(filePath)) + throw new InvalidConfigurationException($"Configuration file not found: {filePath}"); + + return ConfigurationReader.Read(filePath, prefix); + } + + private IReadOnlyDictionary> ParseReplyAttributes( + RadiusReplySection radiusReplySection) + { + if (radiusReplySection?.Attributes?.Any() != true) + return new Dictionary>(); + + var result = new Dictionary>(); + + var groupedAttributes = radiusReplySection.Attributes + .Where(a => !string.IsNullOrWhiteSpace(a.Name)) + .GroupBy(a => a.Name!); + + foreach (var group in groupedAttributes) + { + var attributes = group + .Select(CreateReplyAttribute) + .ToList(); + + result[group.Key] = attributes; + } + + return result; + } + + private IRadiusReplyAttribute CreateReplyAttribute(RadiusAttributeItem item) + { + var attribute = new RadiusReplyAttribute(); + + if (bool.TryParse(item.Sufficient, out var sufficient)) + { + attribute.Sufficient = sufficient; + } + + if (!string.IsNullOrWhiteSpace(item.From)) + { + attribute.Name = item.From; + } + else if (!string.IsNullOrWhiteSpace(item.Value)) + { + attribute.Value = ParseRadiusReplyValue(item.Name!, item.Value); + + if (!string.IsNullOrWhiteSpace(item.When)) + { + ParseWhenCondition(item.When, attribute); + } + } + + return attribute; + } + + private object ParseRadiusReplyValue(string attributeName, string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidConfigurationException("Radius reply value must be specified"); + + var attribute = _dictionary.GetAttribute(attributeName); + + return attribute.Type switch + { + DictionaryAttribute.TypeString or DictionaryAttribute.TypeTaggedString => value, + DictionaryAttribute.TypeInteger or DictionaryAttribute.TypeTaggedInteger => uint.Parse(value), + DictionaryAttribute.TypeIpAddr => IPAddress.Parse(value), + DictionaryAttribute.TypeOctet => value.ToByteArray(), + _ => throw new InvalidConfigurationException($"Unknown attribute type: {attribute.Type}") + }; + } + + private static void ParseWhenCondition(string whenCondition, RadiusReplyAttribute attribute) + { + var parts = whenCondition.Split('=', 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) return; + + var conditionType = parts[0].Trim(); + var values = parts[1] + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(v => v.Trim()) + .ToList(); + + switch (conditionType) + { + case "UserGroup": + attribute.UserGroupCondition = values; + break; + case "UserName": + attribute.UserNameCondition = values; + break; + default: + throw new InvalidConfigurationException($"Unknown condition type: {conditionType}"); + } + } + + private static string GetConfigPrefix(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + return string.Empty; + + var fileName = Path.GetFileNameWithoutExtension(filePath); + return Regex.Replace(fileName, @"\s+", string.Empty); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs new file mode 100644 index 00000000..79861009 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; + +public interface IConfigurationLoader +{ + ServiceConfiguration Load(); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs new file mode 100644 index 00000000..6193bdaa --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs @@ -0,0 +1,160 @@ +using System.ComponentModel; +using System.Xml.Serialization; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +internal class AdapterConfiguration +{ + public AdapterConfiguration() + { + AppSettings = new AppSettingsSection(); + LdapServers = new List(); + RadiusReply = new RadiusReplySection(); + } + public string FileName { get; set; } + public AppSettingsSection AppSettings { get; set; } = new(); + + public List LdapServers { get; set; } = []; + + public RadiusReplySection RadiusReply { get; set; } = new(); +} + +internal class AppSettingsSection +{ + [Description("multifactor-api-url")] + public string MultifactorApiUrl { get; set; } + [Description("multifactor-api-proxy")] + public string MultifactorApiProxy { get; set; } + [Description("multifactor-api-timeout")] + public string MultifactorApiTimeout { get; set; } + [Description("adapter-server-endpoint")] + public string AdapterServerEndpoint { get; set; } + [Description("logging-level")] + public string LoggingLevel { get; set; } + [Description("logging-format")] + public string LoggingFormat { get; set; } + [Description("syslog-use-tls")] + public bool SyslogUseTls { get; set; } + [Description("syslog-server")] + public string SyslogServer { get; set; } + [Description("syslog-format")] + public string SyslogFormat { get; set; } + [Description("syslog-facility")] + public string SyslogFacility { get; set; } + [Description("syslog-app-name")] + public string SyslogAppName { get; set; } + [Description("syslog-framer")] + public string SyslogFramer { get; set; } + [Description("syslog-output-template")] + public string? SyslogOutputTemplate { get; set; } + + [Description("console-log-output-template")] + public string? ConsoleLogOutputTemplate { get; set; } + [Description("file-log-output-template")] + public string? FileLogOutputTemplate { get; set; } + [Description("log-file-max-size-bytes")] + public int? LogFileMaxSizeBytes { get; set; } + [Description("multifactor-nas-identifier")] + public string MultifactorNasIdentifier { get; set; } + [Description("multifactor-shared-secret")] + public string MultifactorSharedSecret { get; set; } + [Description("sign-up-group")] + public string SignUpGroups { get; set; } + [Description("bypass-second-factor-when-api-unreachable")] + public bool BypassSecondFactorWhenApiUnreachable { get; set; } + [Description("first-factor-authentication-source")] + public string FirstFactorAuthenticationSource { get; set; } + [Description("adapter-client-endpoint")] + public string AdapterClientEndpoint { get; set; } + [Description("radius-client-ip")] + public string RadiusClientIp { get; set; } + [Description("radius-client-nas-identifier")] + public string RadiusClientNasIdentifier { get; set; } + [Description("radius-shared-secret")] + public string RadiusSharedSecret { get; set; } + [Description("nps-server-endpoint")] + public string NpsServerEndpoint { get; set; } + [Description("nps-server-timeout")] + public string NpsServerTimeout { get; set; } + [Description("privacy-mode")] + public string Privacy { get; set; } + [Description("pre-authentication-method")] + public string PreAuthenticationMethod { get; set; } + [Description("authentication-cache-lifetime")] + public string AuthenticationCacheLifetime { get; set; } + [Description("invalid-credential-delay")] + public string InvalidCredentialDelay { get; set; } + [Description("calling-station-id-attribute")] + public string CallingStationIdAttribute { get; set; } //TODO not used + [Description("ip-white-list")] + public string IpWhiteList { get; set; } +} + +internal class LdapServerSection +{ + [Description("connection-string")] + public required string ConnectionString { get; set; } + [Description("username")] + public required string Username { get; set; } + [Description("password")] + public required string Password { get; set; } + [Description("bind-timeout-in-seconds")] + public int? BindTimeoutSeconds{ get; set; } + [Description("access-groups")] + public string AccessGroups { get; set; } + [Description("second-fa-groups")] + public string SecondFaGroups { get; set; } + [Description("second-fa-bypass-groups")] + public string SecondFaBypassGroups { get; set; } + [Description("load-nested-groups")] + public bool LoadNestedGroups { get; set; } + [Description("nested-groups-base-dn")] + public string NestedGroupsBaseDn { get; set; } + [Description("authentication-cache-groups")] + public string AuthenticationCacheGroups { get; set; } + [Description("phone-attributes")] + public string PhoneAttributes { get; set; } + [Description("identity-attribute")] + public string IdentityAttribute { get; set; } + [Description("requires-upn")] + public bool RequiresUpn { get; set; } + [Description("enable-trusted-domains")] + public bool TrustedDomainsEnabled { get; set; } + [Description("enable-alternative-suffixes")] + public bool AlternativeSuffixesEnabled { get; set; } + [Description("included-domains")] + public string IncludedDomains { get; set; } + [Description("excluded-domains")] + public string ExcludedDomains { get; set; } + [Description("included-suffixes")] + public string IncludedSuffixes { get; set; } + [Description("excluded-suffixes")] + public string ExcludedSuffixes { get; set; } + [Description("bypass-second-factor-when-api-unreachable-groups")] + public string BypassSecondFactorWhenApiUnreachableGroups { get; set; } +} + +internal class RadiusReplySection +{ + [XmlArray("Attributes")] + [XmlArrayItem("add")] + public List Attributes { get; set; } +} + +internal class RadiusAttributeItem +{ + [Description("name")] + public string Name { get; set; } + + [Description("from")] + public string From { get; set; } + + [Description("value")] + public string Value { get; set; } + + [Description("when")] + public string When { get; set; } + + [Description("sufficient")] + public string Sufficient { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs new file mode 100644 index 00000000..188ca467 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs @@ -0,0 +1,228 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +internal class ClientConfiguration : IClientConfiguration +{ + public string Name { get; set; } + + public string MultifactorNasIdentifier { get; set; } + public string MultifactorSharedSecret { get; set; } + public IReadOnlyList SignUpGroups { get; set; } + public bool BypassSecondFactorWhenApiUnreachable { get; set; } + public AuthenticationSource FirstFactorAuthenticationSource { get; set; } + public IPEndPoint AdapterClientEndpoint { get; set; } + + public IReadOnlyList? RadiusClientIps { get; set; } + public string RadiusClientNasIdentifier { get; set; } + public string RadiusSharedSecret { get; set; } + public IReadOnlyList NpsServerEndpoints { get; set; } + public TimeSpan NpsServerTimeout { get; set; } + + public Privacy Privacy { get; set; } + + public PreAuthMode? PreAuthenticationMethod { get; set; } = PreAuthMode.None; + public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; + public CredentialDelay? InvalidCredentialDelay { get; set; } + public string? CallingStationIdAttribute { get; set; } //TODO not used + public IReadOnlyList IpWhiteList { get; set; } + + public IReadOnlyList? LdapServers { get; set; } + public IReadOnlyDictionary>? ReplyAttributes { get; set; } + + + public static ClientConfiguration FromConfiguration(AdapterConfiguration configurationFile) + { + ArgumentNullException.ThrowIfNull(configurationFile); + const string formatedMessage = "Invalid '{prop}'. Value '{0}' cannot be parsed."; + var dto = new ClientConfiguration + { + Name = configurationFile.FileName, + MultifactorNasIdentifier = + !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorNasIdentifier) + ? configurationFile.AppSettings.MultifactorNasIdentifier + : throw InvalidConfigurationException.For(prop => prop.AppSettings.MultifactorNasIdentifier, + "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName), + MultifactorSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorSharedSecret) + ? configurationFile.AppSettings.MultifactorSharedSecret + : throw InvalidConfigurationException.For(prop => prop.AppSettings.MultifactorSharedSecret, + "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName), + RadiusSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.RadiusSharedSecret) + ? configurationFile.AppSettings.RadiusSharedSecret + : throw InvalidConfigurationException.For(c => c.AppSettings.RadiusSharedSecret, + "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName), + RadiusClientNasIdentifier = configurationFile.AppSettings.RadiusClientNasIdentifier, + CallingStationIdAttribute = configurationFile.AppSettings.CallingStationIdAttribute, + BypassSecondFactorWhenApiUnreachable = configurationFile.AppSettings.BypassSecondFactorWhenApiUnreachable, + Privacy = new Privacy(PrivacyMode.None, []), + PreAuthenticationMethod = PreAuthMode.None, + AuthenticationCacheLifetime = TimeSpan.Zero, + InvalidCredentialDelay = null, + NpsServerEndpoints = [], + NpsServerTimeout = TimeSpan.Parse("00:00:05"), + SignUpGroups = [], + RadiusClientIps = [], + IpWhiteList = [], + }; + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.SignUpGroups)) + if (ConfigurationValueParser.TryParseStringList(configurationFile.AppSettings.SignUpGroups, out var list)) + { + dto.SignUpGroups = list; + } + else + { + var exception = InvalidConfigurationException.For(c => c.AppSettings.SignUpGroups, + formatedMessage, configurationFile.AppSettings.SignUpGroups); + StartupLogger.Warning(exception.Message); + dto.SignUpGroups = []; + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.RadiusClientIp)) + if (ConfigurationValueParser.TryParseIpAddress(configurationFile.AppSettings.RadiusClientIp, + out var address)) + { + dto.RadiusClientIps = address; + } + else + { + var exception = InvalidConfigurationException.For(c => c.AppSettings.RadiusClientIp, + formatedMessage, configurationFile.AppSettings.RadiusClientIp); + StartupLogger.Warning(exception.Message); + dto.RadiusClientIps = []; + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.NpsServerEndpoint)) + if (ConfigurationValueParser.TryParseEndpoints(configurationFile.AppSettings.NpsServerEndpoint, + out var npsServerEndpoints)) + { + dto.NpsServerEndpoints = npsServerEndpoints; + } + else + { + var exception = InvalidConfigurationException.For(c => c.AppSettings.NpsServerEndpoint, + formatedMessage, configurationFile.AppSettings.NpsServerEndpoint); + StartupLogger.Warning(exception.Message); + dto.NpsServerEndpoints = []; + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.NpsServerTimeout)) + if (ConfigurationValueParser.TryParseTimeout(configurationFile.AppSettings.NpsServerTimeout, + out var timeout)) + { + dto.NpsServerTimeout = timeout.Value; + } + else + { + dto.NpsServerTimeout = TimeSpan.Parse("00:00:05"); + var exception = InvalidConfigurationException.For(c => c.AppSettings.NpsServerTimeout, + formatedMessage, configurationFile.AppSettings.NpsServerTimeout); + StartupLogger.Warning(exception.Message); + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.Privacy)) + if (ConfigurationValueParser.TryParsePrivacyModeWithFields(configurationFile.AppSettings.Privacy, + out var privacy)) + { + dto.Privacy = privacy; + } + else + { + dto.Privacy = new(PrivacyMode.None, []); + var exception = InvalidConfigurationException.For(c => c.AppSettings.Privacy, + formatedMessage, configurationFile.AppSettings.Privacy); + StartupLogger.Warning(exception.Message); + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.PreAuthenticationMethod)) + if (ConfigurationValueParser.TryParseEnum( + configurationFile.AppSettings.PreAuthenticationMethod, + out var mode)) + { + dto.PreAuthenticationMethod = mode; + } + else + { + dto.PreAuthenticationMethod = PreAuthMode.None; + var exception = InvalidConfigurationException.For(c => c.AppSettings.PreAuthenticationMethod, + formatedMessage, configurationFile.AppSettings.PreAuthenticationMethod); + StartupLogger.Warning(exception.Message); + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.AuthenticationCacheLifetime)) + if (ConfigurationValueParser.TryParseTimeSpan(configurationFile.AppSettings.AuthenticationCacheLifetime, + out var span)) + { + dto.AuthenticationCacheLifetime = span; + } + else + { + dto.AuthenticationCacheLifetime = TimeSpan.Zero; + var exception = InvalidConfigurationException.For(c => c.AppSettings.AuthenticationCacheLifetime, + formatedMessage, configurationFile.AppSettings.AuthenticationCacheLifetime); + StartupLogger.Warning(exception.Message); + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.IpWhiteList)) + if (ConfigurationValueParser.TryParseIpRanges(configurationFile.AppSettings.IpWhiteList, + out var ipWhiteList)) + { + dto.IpWhiteList = ipWhiteList; + } + else + { + dto.IpWhiteList = []; + var exception = InvalidConfigurationException.For(c => c.AppSettings.IpWhiteList, + formatedMessage, configurationFile.AppSettings.IpWhiteList); + StartupLogger.Warning(exception.Message); + } + + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.InvalidCredentialDelay)) + if (ConfigurationValueParser.TryParseDelaySettings(configurationFile.AppSettings.InvalidCredentialDelay, + out var tuple)) + { + dto.InvalidCredentialDelay = tuple; + } + else + { + var exception = InvalidConfigurationException.For(c => c.AppSettings.InvalidCredentialDelay, + formatedMessage, configurationFile.AppSettings.InvalidCredentialDelay); + StartupLogger.Warning(exception.Message); + } + + var firstFactorAuthenticationSource = + !string.IsNullOrWhiteSpace(configurationFile.AppSettings.FirstFactorAuthenticationSource) + ? configurationFile.AppSettings.FirstFactorAuthenticationSource + : throw InvalidConfigurationException.For(prop => prop.AppSettings.FirstFactorAuthenticationSource, + "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName); + + dto.FirstFactorAuthenticationSource = + ConfigurationValueParser.TryParseEnum(firstFactorAuthenticationSource, out var source) + ? source + : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, + "Error while cast property '{prop}'. Value: {0}. Config name: '{1}'", + firstFactorAuthenticationSource, configurationFile.FileName); + + var adapterClientEndpoint = + dto.FirstFactorAuthenticationSource != AuthenticationSource.Radius || + !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint) + ? configurationFile.AppSettings.AdapterClientEndpoint + : throw InvalidConfigurationException.For(c => c.AppSettings.AdapterClientEndpoint, + "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName); + if (!string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint)) + dto.AdapterClientEndpoint = + ConfigurationValueParser.TryParseEndpoint(adapterClientEndpoint, out var endpoint) + ? endpoint! + : throw InvalidConfigurationException.For(c => c.AppSettings.AdapterClientEndpoint, + "Error while cast property '{prop}'. Value: {0}. Config name: '{1}'", adapterClientEndpoint, + configurationFile.FileName); + ; + return dto; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryAttribute.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryAttribute.cs index cfc0a768..802d4b9d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryAttribute.cs @@ -22,7 +22,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class DictionaryAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryVendorAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryVendorAttribute.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryVendorAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryVendorAttribute.cs index d1cacb7f..f33783be 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryVendorAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryVendorAttribute.cs @@ -22,7 +22,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class DictionaryVendorAttribute : DictionaryAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/VendorSpecificAttribute.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/VendorSpecificAttribute.cs index 4c16b499..187d2ab4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/VendorSpecificAttribute.cs @@ -24,7 +24,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class VendorSpecificAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/IRadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs similarity index 82% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/IRadiusDictionary.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs index ca5fb472..29988927 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/IRadiusDictionary.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs @@ -1,4 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary { public interface IRadiusDictionary { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs new file mode 100644 index 00000000..9dac683c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs @@ -0,0 +1,143 @@ +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using DictionaryAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes.DictionaryAttribute; +using DictionaryVendorAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes.DictionaryVendorAttribute; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary +{ + public class RadiusDictionary : IRadiusDictionary + { + private readonly Dictionary _attributes = new(); + private readonly Dictionary<(uint VendorId, byte VendorCode), DictionaryVendorAttribute> _vendorAttributes = new(); + private readonly Dictionary _attributeNames = new(); + private readonly ApplicationVariables _variables; + private readonly string _filePath; + + public RadiusDictionary(ApplicationVariables variables, string? filePath = null) + { + _variables = variables; + _filePath = ResolveFilePath(filePath); + } + + private string ResolveFilePath(string? filePath) + { + if (!string.IsNullOrEmpty(filePath) && Path.IsPathRooted(filePath)) + return filePath; + + var basePath = string.IsNullOrEmpty(_variables.AppPath) + ? AppDomain.CurrentDomain.BaseDirectory + : _variables.AppPath; + + var relativePath = string.IsNullOrEmpty(filePath) + ? Path.Combine("content", "radius.dictionary") + : filePath; + + return Path.Combine(basePath, relativePath); + } + + public void Read() + { + if (!File.Exists(_filePath)) + throw new FileNotFoundException($"Dictionary file not found: {_filePath}"); + + using var reader = new StreamReader(_filePath, Encoding.UTF8); + + string? line; + while ((line = reader.ReadLine()) != null) + { + ProcessLine(line.Trim()); + } + + GetInfo(); + } + + private void ProcessLine(string line) + { + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) + return; + + var parts = SplitLine(line); + + if (parts.Length < 2) return; + + switch (parts[0].ToUpperInvariant()) + { + case "ATTRIBUTE": + ParseAttribute(parts); + break; + case "VENDORSPECIFICATTRIBUTE": + ParseVendorAttribute(parts); + break; + } + } + + private static string[] SplitLine(string line) + { + return line.Split(['\t', ' ', '\''], StringSplitOptions.RemoveEmptyEntries); + } + + private void ParseAttribute(string[] parts) + { + if (parts.Length < 4) return; + + if (!byte.TryParse(parts[1], out byte typeCode)) + return; + + var name = parts[2]; + var dataType = parts[3]; + + var attribute = new DictionaryAttribute(name, typeCode, dataType); + + // Обновляем существующие записи + _attributes[typeCode] = attribute; + _attributeNames[name] = attribute; + } + + private void ParseVendorAttribute(string[] parts) + { + if (parts.Length < 5) return; + + if (!uint.TryParse(parts[1], out uint vendorId) || + !byte.TryParse(parts[2], out byte vendorCode)) + return; + + var name = parts[3]; + var dataType = parts[4]; + + var vsa = new DictionaryVendorAttribute(vendorId, name, vendorCode, dataType); + + var key = (vendorId, vendorCode); + + // Обновляем существующие записи + _vendorAttributes[key] = vsa; + _attributeNames[name] = vsa; + } + + public string GetInfo() + { + return $"Parsed {_attributes.Count} attributes and {_vendorAttributes.Count} vendor attributes"; + } + + public DictionaryVendorAttribute? GetVendorAttribute(uint vendorId, byte vendorCode) + { + var key = (vendorId, vendorCode); + return _vendorAttributes.TryGetValue(key, out var attribute) ? attribute : null; + } + + public DictionaryAttribute GetAttribute(byte code) + { + if (_attributes.TryGetValue(code, out var attribute)) + return attribute; + + throw new KeyNotFoundException($"Attribute with code {code} not found"); + } + + public DictionaryAttribute GetAttribute(string name) + { + if (_attributeNames.TryGetValue(name, out var attribute)) + return attribute; + + throw new KeyNotFoundException($"Attribute with name '{name}' not found"); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs new file mode 100644 index 00000000..0068b83b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs @@ -0,0 +1,115 @@ +using Elastic.CommonSchema; +using Microsoft.AspNetCore.Hosting.Server; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +internal class LdapServerConfiguration : ILdapServerConfiguration +{ + public string ConnectionString { get; init; } + public string Username { get; init; } + public string Password { get; init; } + public int BindTimeoutSeconds{ get; init; } + public IReadOnlyList AccessGroups { get; init; } + public IReadOnlyList SecondFaGroups { get; init; } + public IReadOnlyList SecondFaBypassGroups { get; init; } + public bool LoadNestedGroups { get; init; } + public IReadOnlyList NestedGroupsBaseDns { get; init; } + public IReadOnlyList AuthenticationCacheGroups { get; init; } + public IReadOnlyList PhoneAttributes { get; init; } + public string IdentityAttribute { get; init; } + public bool RequiresUpn { get; init; }//TODO not used + public bool TrustedDomainsEnabled { get; init; }//TODO not used + public bool AlternativeSuffixesEnabled { get; init; }//TODO not used + public IReadOnlyList IncludedDomains { get; init; }//TODO not used + public IReadOnlyList ExcludedDomains { get; init; }//TODO not used + public IReadOnlyList IncludedSuffixes { get; init; } + public IReadOnlyList ExcludedSuffixes { get; init; } + public IReadOnlyList BypassSecondFactorWhenApiUnreachableGroups { get; init; } + + public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapServerSection, string fileName) + { + if (ldapServerSection is { TrustedDomainsEnabled: true, RequiresUpn: false }) + throw new InvalidConfigurationException($"Config name: '{fileName}', LDAP server: '{ldapServerSection.ConnectionString}'. To use trusted domains also set 'requires-upn' to 'true'."); + + if (!string.IsNullOrWhiteSpace(ldapServerSection.IncludedDomains) && !string.IsNullOrWhiteSpace(ldapServerSection.ExcludedDomains)) + throw new InvalidConfigurationException($"Config name: '{fileName}', LDAP server: '{ldapServerSection.ConnectionString}'. Simultaneous use of 'included-domains' and 'excluded-domains' is not allowed."); + + if (!string.IsNullOrWhiteSpace(ldapServerSection.IncludedSuffixes) && !string.IsNullOrWhiteSpace(ldapServerSection.ExcludedSuffixes)) + throw new InvalidConfigurationException($"Config name: '{fileName}', LDAP server: '{ldapServerSection.ConnectionString}'. Simultaneous use of 'included-suffixes' and 'excluded-suffixes' is not allowed."); + var dto = new LdapServerConfiguration + { + ConnectionString = !string.IsNullOrWhiteSpace(ldapServerSection.ConnectionString) ? ldapServerSection.ConnectionString : + throw InvalidConfigurationException.For(prop => prop.LdapServers[0].ConnectionString, "Property '{prop}' is required. Config name: '{0}'", fileName), + + Username = !string.IsNullOrWhiteSpace(ldapServerSection.Username) ? ldapServerSection.Username : + throw InvalidConfigurationException.For(prop => prop.LdapServers[0].Username, "Property '{prop}' is required. Config name: '{0}'", fileName), + + Password = !string.IsNullOrWhiteSpace(ldapServerSection.Password) ? ldapServerSection.Password : + throw InvalidConfigurationException.For(prop => prop.LdapServers[0].Password, "Property '{prop}' is required. Config name: '{0}'", fileName), + + BindTimeoutSeconds = ldapServerSection.BindTimeoutSeconds ?? 30, + AccessGroups = + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.AccessGroups, + out var accessGroups) + ? accessGroups + : [], + SecondFaGroups = + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.SecondFaGroups, + out var secondFaGroups) + ? secondFaGroups + : [], + SecondFaBypassGroups = + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.SecondFaBypassGroups, out var secondFaBypassGroups) + ? secondFaBypassGroups + : [], + LoadNestedGroups = ldapServerSection.LoadNestedGroups, + NestedGroupsBaseDns = + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDn, out var nestedGroupsBaseDn) + ? nestedGroupsBaseDn + : [], + AuthenticationCacheGroups = + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.AuthenticationCacheGroups, out var authenticationCacheGroups) + ? authenticationCacheGroups + : [], + PhoneAttributes = + ConfigurationValueParser.TryParseStringList(ldapServerSection.PhoneAttributes, + out var phoneAttributes) + ? phoneAttributes + : [], + IdentityAttribute = ldapServerSection.IdentityAttribute, + RequiresUpn = ldapServerSection.RequiresUpn, + TrustedDomainsEnabled = ldapServerSection.TrustedDomainsEnabled, + AlternativeSuffixesEnabled = ldapServerSection.AlternativeSuffixesEnabled, + IncludedDomains = + ConfigurationValueParser.TryParseStringList(ldapServerSection.IncludedDomains, + out var includedDomains) + ? includedDomains + : [], + ExcludedDomains = + ConfigurationValueParser.TryParseStringList(ldapServerSection.ExcludedDomains, + out var excludedDomains) + ? excludedDomains + : [], + IncludedSuffixes = + ConfigurationValueParser.TryParseStringList(ldapServerSection.IncludedSuffixes, + out var includedSuffixes) + ? includedSuffixes + : [], + ExcludedSuffixes = + ConfigurationValueParser.TryParseStringList(ldapServerSection.ExcludedSuffixes, + out var excludedSuffixes) + ? excludedSuffixes + : [], + BypassSecondFactorWhenApiUnreachableGroups = ConfigurationValueParser.TryParseStringList(ldapServerSection.BypassSecondFactorWhenApiUnreachableGroups, + out var bypassSecondFactorWhenApiUnreachableGroups) + ? bypassSecondFactorWhenApiUnreachableGroups + : [] + + }; + return dto; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RadiusReplyAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RadiusReplyAttribute.cs new file mode 100644 index 00000000..97651af2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RadiusReplyAttribute.cs @@ -0,0 +1,14 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +public class RadiusReplyAttribute : IRadiusReplyAttribute +{ + public string Name { get; set; } = string.Empty; + public object Value { get; set; } = string.Empty; + public IReadOnlyList UserGroupCondition { get; set; } = []; + public IReadOnlyList UserNameCondition { get; set; } = []; + public bool Sufficient { get; set; } + public bool IsMemberOf => Name?.ToLower() == "memberof"; + public bool FromLdap => !string.IsNullOrWhiteSpace(Name); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs new file mode 100644 index 00000000..741e54ab --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs @@ -0,0 +1,62 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +internal class RootConfiguration : IRootConfiguration +{ + public IReadOnlyList MultifactorApiUrls { get; set; } + public string? MultifactorApiProxy { get; set; } + public TimeSpan MultifactorApiTimeout { get; set; } + public IPEndPoint? AdapterServerEndpoint { get; set; } + public string LoggingLevel { get; set; } + public string? LoggingFormat { get; set; } + public bool SyslogUseTls { get; set; } + public string? SyslogServer { get; set; } + public string? SyslogFormat { get; set; } + public string? SyslogFacility { get; set; } + public string SyslogAppName { get; set; } + public string? SyslogFramer { get; set; } + public string? SyslogOutputTemplate { get; set; } + + public string? ConsoleLogOutputTemplate { get; set; } + public string? FileLogOutputTemplate { get; set; } + public int LogFileMaxSizeBytes { get; set; } + + public static RootConfiguration FromConfiguration(AdapterConfiguration configurationFile) + { + ArgumentNullException.ThrowIfNull(configurationFile); + var conf = new RootConfiguration + { + MultifactorApiProxy = configurationFile.AppSettings?.MultifactorApiProxy, + MultifactorApiTimeout = ConfigurationValueParser.TryParseTimeout( + configurationFile.AppSettings?.MultifactorApiTimeout, out var span) + ? span!.Value : TimeSpan.FromSeconds(65), + LoggingFormat = configurationFile.AppSettings?.LoggingFormat, + SyslogUseTls = configurationFile.AppSettings?.SyslogUseTls ?? false, + SyslogServer = configurationFile.AppSettings?.SyslogServer, + SyslogFormat = configurationFile.AppSettings?.SyslogFormat, + SyslogFacility = configurationFile.AppSettings?.SyslogFacility, + SyslogAppName = configurationFile.AppSettings?.SyslogAppName ?? "multifactor-radius", + SyslogFramer = configurationFile.AppSettings?.SyslogFramer, + SyslogOutputTemplate = configurationFile.AppSettings?.SyslogOutputTemplate, + ConsoleLogOutputTemplate = configurationFile.AppSettings?.ConsoleLogOutputTemplate, + FileLogOutputTemplate = configurationFile.AppSettings?.FileLogOutputTemplate, + LogFileMaxSizeBytes = configurationFile.AppSettings?.LogFileMaxSizeBytes ?? 1073741824, + LoggingLevel = configurationFile.AppSettings?.LoggingLevel ?? "Debug" + }; + var urls = !string.IsNullOrWhiteSpace(configurationFile.AppSettings?.MultifactorApiUrl) ? configurationFile.AppSettings.MultifactorApiUrl : + throw InvalidConfigurationException.For(prop => prop.AppSettings.MultifactorApiUrl, "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName); + conf.MultifactorApiUrls = ConfigurationValueParser.TryParseUrls(urls, out var parsedUrls) ? parsedUrls : + throw InvalidConfigurationException.For(prop => prop.AppSettings.MultifactorApiUrl, $"Invalid {{prop}}: '{urls}'", configurationFile.FileName); + + var endpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings?.AdapterServerEndpoint) ? configurationFile.AppSettings.AdapterServerEndpoint : throw new InvalidConfigurationException(nameof(conf.AdapterServerEndpoint)); + + conf.AdapterServerEndpoint = ConfigurationValueParser.TryParseEndpoint(endpoint, out var point) + ? point + : throw new InvalidConfigurationException($"Invalid 'adapter-server-endpoint': '{endpoint}'", configurationFile.FileName); + return conf; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs new file mode 100644 index 00000000..b2457b60 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs @@ -0,0 +1,309 @@ +using System.Globalization; +using System.Net; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +internal static class ConfigurationValueParser +{ + public static bool TryParseEnum(string? value, out T result, T defaultValue = default) where T : struct + { + result = defaultValue; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (Enum.TryParse(value, true, out var parsedResult)) + { + result = parsedResult; + return true; + } + + return false; + } + + public static bool TryParseTimeSpan(string? value, out TimeSpan result, TimeSpan? defaultValue = null) + { + result = defaultValue ?? TimeSpan.Zero; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (TimeSpan.TryParse(value, out var parsedResult)) + { + result = parsedResult; + return true; + } + + return false; + } + + public static bool TryParseTimeout(string? value, out TimeSpan? result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var forced = value.EndsWith('!'); + if (forced) + value = value.TrimEnd('!'); + + if (!TimeSpan.TryParseExact(value, @"hh\:mm\:ss", null, TimeSpanStyles.None, out var parsedResult)) + return false; + + if (parsedResult == TimeSpan.Zero) + { + result = Timeout.InfiniteTimeSpan; + return true; + } + + // Логирование если timeout слишком маленький + var recommendedMin = TimeSpan.FromSeconds(65); + if (parsedResult < recommendedMin) + { + if (forced) + { + StartupLogger.Warning( + "Timeout {Timeout}s is less than recommended minimum {Recommended}s", + parsedResult.TotalSeconds, recommendedMin.TotalSeconds); + result = parsedResult; + } + else + { + StartupLogger.Warning( + "Timeout {Timeout}s is less than recommended minimum {Recommended}s. Use 'value!' to force", + parsedResult.TotalSeconds, recommendedMin.TotalSeconds); + result = recommendedMin; + } + } + else + { + result = parsedResult; + } + + return true; + } + + public static bool TryParseEndpoint(string? value, out IPEndPoint? result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (IPEndPoint.TryParse(value, out var endpoint)) + { + result = endpoint; + return true; + } + + return false; + } + + public static bool TryParseEndpoints(string? value, out IPEndPoint[] result, char separator = ';') + { + result = []; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var endpoints = new List(); + var parts = value.Split(separator, StringSplitOptions.RemoveEmptyEntries); + + foreach (var endpoint in parts) + { + if (IPEndPoint.TryParse(endpoint, out var endpointResult)) + { + endpoints.Add(endpointResult); + } + else + { + return false; + } + } + + result = endpoints.ToArray(); + return true; + } + + public static bool TryParseIpAddress(string? value, out IReadOnlyList? result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var addresses = new List(); + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!IPAddress.TryParse(trimmed, out var ipAddress)) + { + return false; + } + addresses.Add(ipAddress); + } + + result = addresses; + return true; + } + + public static bool TryParseUrls(string? value, out IReadOnlyList result) + { + result = []; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var urls = new List(); + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)) + { + return false; + } + + urls.Add(uri); + } + + result = urls; + return true; + } + + public static bool TryParseIpRanges(string? value, out IReadOnlyList result) + { + result = []; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var ranges = new List(); + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!IPAddressRange.TryParse(trimmed, out var range)) + { + return false; + } + + ranges.Add(range); + } + + result = ranges; + return true; + } + + public static bool TryParseDistinguishedNames(string? value, out IReadOnlyList result) + { + result = []; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var names = new List(); + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + try + { + var trimmed = part.Trim(); + if (!string.IsNullOrEmpty(trimmed)) + { + names.Add(new DistinguishedName(trimmed)); + } + } + catch (ArgumentException) + { + return false; + } + } + + result = names; + return true; + } + + public static bool TryParseStringList(string? value, out IReadOnlyList result, char separator = ';') + { + result = []; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + result = value.Split(separator, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + + return true; + } + + public static bool TryParsePrivacyModeWithFields(string? value, out Privacy? result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + var parts = value.Split(':', 2); + + if (!TryParseEnum(parts[0], out PrivacyMode mode, PrivacyMode.None)) + return false; + + if (parts.Length == 1) + { + result = new Privacy(mode, []); + return true; + } + + var fields = parts[1].Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(f => f.Trim()) + .Distinct() + .ToArray(); + + result = new Privacy(mode, fields); + return true; + } + + public static bool TryParseDelaySettings(string? value, out CredentialDelay result) + { + result = new CredentialDelay(0, 0); + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (int.TryParse(value, out var delay)) + { + if (delay < 0) + return false; + + result = new CredentialDelay(delay,delay); + return true; + } + + var splitted = value.Split(['-'], StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2) + return false; + + var values = splitted.Select(x => int.TryParse(x, out var d) ? d : -1).ToArray(); + if (values.Any(x => x < 0)) + return false; + + result = new CredentialDelay(values[0], values[1]); + return true; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs new file mode 100644 index 00000000..ab8508fe --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public static class ConfigurationBuilderExtensions +{ + public static IConfigurationBuilder AddXmlConfig( + this IConfigurationBuilder builder, + string path) + { + return builder.Add(new XmlConfigurationSource(path)); + } + + public static IConfigurationBuilder AddEnvironmentVariables( + this IConfigurationBuilder builder, + string prefix) + { + return builder.Add(new PrefixEnvironmentVariablesConfigurationSource(prefix)); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs new file mode 100644 index 00000000..88bf768c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Configuration; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +internal static class ConfigurationReader +{ + internal static AdapterConfiguration? Read(string filePath, string prefix = null) + { + var builder = new ConfigurationBuilder() + .AddXmlConfig(filePath) + .AddEnvironmentVariables($"RAD_{prefix}") + .Build(); + + try + { + var config = builder.Get(); + if (config == null) return null; + config.FileName = Path.GetFileNameWithoutExtension(filePath); + return config; + } + catch (Exception ex) + { + StartupLogger.Error(ex, "Error reading configuration file:{0}", ex.Message); + throw; + } + + } +} + \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationProvider.cs new file mode 100644 index 00000000..be39b053 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationProvider.cs @@ -0,0 +1,39 @@ +using System.Collections; +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public class PrefixEnvironmentVariablesConfigurationProvider : ConfigurationProvider +{ + private readonly string _prefix; + + public PrefixEnvironmentVariablesConfigurationProvider(string prefix) + { + _prefix = prefix ?? string.Empty; + } + + public override void Load() + { + Data.Clear(); + + var envVars = Environment.GetEnvironmentVariables(); + + foreach (DictionaryEntry entry in envVars) + { + var key = entry.Key.ToString(); + if (!string.IsNullOrEmpty(key) && key.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) + { + var value = entry.Value?.ToString(); + if (value != null) + { + // Убираем префикс и преобразуем в формат конфигурации + var configKey = key.Substring(_prefix.Length) + .Replace("__", ":") // Двойное подчеркивание -> разделитель + .ToLower(); // Все в нижний регистр для консистентности + + Data[configKey] = value; + } + } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationSource.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationSource.cs new file mode 100644 index 00000000..04c65cd4 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationSource.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public class PrefixEnvironmentVariablesConfigurationSource : IConfigurationSource +{ + private readonly string _prefix; + + public PrefixEnvironmentVariablesConfigurationSource(string prefix) + { + _prefix = prefix; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + => new PrefixEnvironmentVariablesConfigurationProvider(_prefix); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationProvider.cs new file mode 100644 index 00000000..96daa906 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationProvider.cs @@ -0,0 +1,213 @@ + +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md +using System.Text; +using System.Xml.Linq; +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public class XmlConfigurationProvider : ConfigurationProvider, IConfigurationSource +{ + private const string AppSettingsElement = "appSettings"; + + private string _path; + + public XmlConfigurationProvider(string path) + { + _path = path ?? throw new ArgumentNullException(nameof(path)); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; + + public override void Load() + { + try + { + LoadInternal(); + } + catch (Exception ex) + { + throw new Exception($"Failed to load configuration file '{_path}'", ex); + } + } + + private void LoadInternal() + { + var xml = XDocument.Load(_path); + var root = xml.Root; + + if (root is null) + { + throw new Exception("Root XML element not found"); + } + + var appSettings = root.Element(AppSettingsElement); + if (appSettings != null) + { + var appSettingsElements = appSettings.Elements().ToArray(); + XmlAssert.HasUniqueElements(appSettingsElements, x => x.Attribute("key")?.Value); + + FillAppSettingsSection(appSettingsElements); + } + + var sections = root.Elements() + .Where(x => x.Name != AppSettingsElement) + .ToArray(); + XmlAssert.HasUniqueElements(sections, x => x.Name); + + foreach (var section in sections) + { + FillSection(section); + } + } + + private void FillAppSettingsSection(XElement[] appSettingsElements) + { + for (var i = 0; i < appSettingsElements.Length; i++) + { + var key = XmlAssert.HasAttribute(appSettingsElements[i], "key"); + var value = XmlAssert.HasAttribute(appSettingsElements[i], "value"); + + var newKey = $"{AppSettingsElement}:{ToPascalCase(key)}"; + Data.Add(newKey, value); + } + } + + private void FillSection(XElement section, string parentKey = null, string postfix = null) + { + var sectionKey = section.Name.ToString(); + if (parentKey != null) + { + sectionKey = $"{parentKey}:{sectionKey}"; + } + + if (postfix != null) + { + sectionKey = $"{sectionKey}:{postfix}"; + } + + if (section.HasAttributes) + { + foreach (var attr in section.Attributes()) + { + var attrKey = $"{sectionKey}:{ToPascalCase(attr.Name.LocalName)}"; + Data[attrKey] = attr.Value; + } + } + + if (!section.HasElements) + { + return; + } + + var groups = section.Elements().GroupBy(x => x.Name); + foreach (var group in groups) + { + if (group.Count() == 1) + { + FillSection(group.First(), sectionKey); + continue; + } + + var index = 0; + foreach (var arrEntry in group) + { + FillSection(arrEntry, sectionKey, index.ToString()); + index++; + } + } + } + + private static string ToPascalCase(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return input; + + var separators = new[] { '-', '_', '.', ' ' }; + var parts = input.Split(separators, StringSplitOptions.RemoveEmptyEntries); + var result = new StringBuilder(); + + foreach (var part in parts) + { + if (part.Length > 0) + { + result.Append(char.ToUpperInvariant(part[0])); + if (part.Length > 1) + { + result.Append(part.Substring(1).ToLowerInvariant()); + } + } + } + + return result.ToString(); + } +} + +internal static class XmlAssert +{ + /// + /// Explodes if the collection contains duplicates. + /// + /// Selector key type. + /// Source collection. + /// Grouping selector. + /// + /// + public static void HasUniqueElements(IEnumerable elements, Func keySelector) + { + if (elements is null) + { + throw new ArgumentNullException(nameof(elements)); + } + + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + + var duplicates = elements + .GroupBy(keySelector) + .Where(x => x.Count() > 1) + .Select(x => $"'{x.Key}'") + .ToArray(); + + if (duplicates.Length != 0) + { + var d = string.Join(", ", duplicates); + throw new Exception($"Invalid xml config. Duplicates found: {d}"); + } + } + + /// + /// Returns attribute value or throws if the attribute does not exist. + /// + /// Target element. + /// Attribute to get value from. + /// + /// + /// + /// + public static string HasAttribute(XElement element, string attribute) + { + if (element is null) + { + throw new ArgumentNullException(nameof(element)); + } + + if (string.IsNullOrWhiteSpace(attribute)) + { + throw new ArgumentException($"'{nameof(attribute)}' cannot be null or whitespace.", nameof(attribute)); + } + + var attr = element.Attribute(attribute); + if (attr == null) + { + throw new Exception($"Invalid xml config: required attribute 'value' not found. Target element: {element}"); + } + + return attr.Value; + } +} + diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationSource.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationSource.cs new file mode 100644 index 00000000..ab3939a0 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationSource.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public class XmlConfigurationSource : IConfigurationSource +{ + private readonly string _path; + + public XmlConfigurationSource(string path) => _path = path; + + public IConfigurationProvider Build(IConfigurationBuilder builder) + => new XmlConfigurationProvider(_path); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs new file mode 100644 index 00000000..d1282e56 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -0,0 +1,170 @@ +using System.Security.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; +using Multifactor.Core.Ldap.LdapGroup.Load; +using Multifactor.Core.Ldap.LdapGroup.Membership; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.PacketHandler; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Udp; +using Multifactor.Radius.Adapter.v2.Infrastructure.Cache; +using Multifactor.Radius.Adapter.v2.Infrastructure.Cache.AuthenticatedClientCache; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; +using Polly; +using Serilog; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Extensions; + +public static class InfrastructureExtensions +{ + public static void AddConfiguration(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(prov => + { + var dict = prov.GetRequiredService(); + dict.Read(); + return dict; + }); + services.AddSingleton(); + + services.AddSingleton(provider => + { + var manager = provider.GetRequiredService(); + return manager.Load(); + }); + } + + public static void AddRadiusUdpClient(this IServiceCollection services) + { + services.AddSingleton(serviceProvider => + { + var config = serviceProvider.GetRequiredService(); + var endpoint = config.RootConfiguration.AdapterServerEndpoint; + var logger = serviceProvider.GetService>(); + + return new CustomUdpClient(endpoint, logger); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + } + + + public static void AddMultifactorApi(this IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(); + services.AddHttpClient("multifactor-api") + .ConfigureHttpClient((serviceProvider, client) => + { + var config = serviceProvider.GetRequiredService(); + if (config.RootConfiguration.MultifactorApiUrls.Any()) + { + var primaryUrl = config.RootConfiguration.MultifactorApiUrls[0]; + client.BaseAddress = primaryUrl; + client.Timeout = config.RootConfiguration.MultifactorApiTimeout; + } + }) + .AddPolicyHandler((serviceProvider, request) => { + + var config = serviceProvider.GetRequiredService(); + var timeout = config.RootConfiguration.MultifactorApiTimeout; + var selector = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + return Policy + .Handle() + .OrResult(response => !response.IsSuccessStatusCode && (int)response.StatusCode >= 500) + .RetryAsync( + retryCount: config.RootConfiguration.MultifactorApiUrls.Count - 1, + onRetryAsync: async (outcome, retryNumber, context) => + { + logger.LogWarning("Attempt {RetryNumber} failed. Trying next endpoint. Error: {Error}", + retryNumber, + outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); + + // retry endpoint + var fallbackUrl = await selector.GetNextEndpointAsync(); + request.RequestUri = new Uri(fallbackUrl, request.RequestUri!.PathAndQuery); + }) + .WrapAsync(Policy.TimeoutAsync(timeout)); + } + ) + .AddHttpMessageHandler() + .ConfigurePrimaryHttpMessageHandler(provider => + { + var config = provider.GetRequiredService(); + var handler = new HttpClientHandler + { + MaxConnectionsPerServer = 100, + SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 + }; + + if (string.IsNullOrWhiteSpace(config.RootConfiguration.MultifactorApiProxy)) + return handler; + + if (!WebProxyFactory.TryCreateWebProxy(config.RootConfiguration.MultifactorApiProxy, out var webProxy)) + throw new Exception( + "Unable to initialize WebProxy. Please, check whether multifactor-api-proxy URI is valid."); + + handler.Proxy = webProxy; + + return handler; + }); + + services.AddSingleton(); + } + + public static void AddAdapterLogging(this IServiceCollection services) + { + services.AddSerilog((provider, loggerConfiguration) => + { + var serviceConfiguration = provider.GetRequiredService(); + SerilogLoggerFactory.CreateLogger(loggerConfiguration, serviceConfiguration.RootConfiguration); + }); + } + + public static void AddLdap(this IServiceCollection services) + { + services.AddSingleton(LdapConnectionFactory.Create()); + services.AddSingleton((prov) => new CustomLdapConnectionFactory()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + } + + public static void AddInfraServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/CustomCompactJsonFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/CustomCompactJsonFormatter.cs similarity index 100% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/CustomCompactJsonFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/CustomCompactJsonFormatter.cs diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogJsonFormatterTypes.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogJsonFormatterTypes.cs similarity index 100% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogJsonFormatterTypes.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogJsonFormatterTypes.cs diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs similarity index 71% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogLoggerFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index 98918af8..06469028 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -1,61 +1,60 @@ using Elastic.CommonSchema.Serilog; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; using Serilog; using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Compact; using Serilog.Sinks.Syslog; +using Serilog.Sinks.SystemConsole.Themes; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Logging; public static class SerilogLoggerFactory { - public static ILogger CreateLogger(RadiusAdapterConfiguration rootConfiguration) + public static LoggerConfiguration CreateLogger(LoggerConfiguration loggerConfiguration, IRootConfiguration rootConfiguration) { ArgumentNullException.ThrowIfNull(rootConfiguration); var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Information); - var loggerConfiguration = new LoggerConfiguration() - .MinimumLevel.ControlledBy(levelSwitch) + loggerConfiguration.MinimumLevel.ControlledBy(levelSwitch) .MinimumLevel.Override("Microsoft.Extensions.Http.DefaultHttpClientFactory", LogEventLevel.Warning) .Enrich.FromLogContext(); ConfigureLogging( loggerConfiguration, - rootConfiguration.AppSettings.LoggingFormat, - rootConfiguration.AppSettings.SyslogOutputTemplate, - rootConfiguration.AppSettings.ConsoleLogOutputTemplate); + rootConfiguration.LoggingFormat, + rootConfiguration.FileLogOutputTemplate, + rootConfiguration.LogFileMaxSizeBytes, + rootConfiguration.ConsoleLogOutputTemplate); ConfigureSyslog(loggerConfiguration, - rootConfiguration.AppSettings.SyslogServer, - rootConfiguration.AppSettings.SyslogFormat, - rootConfiguration.AppSettings.SyslogOutputTemplate, - rootConfiguration.AppSettings.SyslogFacility, - rootConfiguration.AppSettings.SyslogFramer, - rootConfiguration.AppSettings.SyslogAppName, - rootConfiguration.AppSettings.SyslogUseTls + rootConfiguration.SyslogServer, + rootConfiguration.SyslogFormat, + rootConfiguration.SyslogOutputTemplate, + rootConfiguration.SyslogFacility, + rootConfiguration.SyslogFramer, + rootConfiguration.SyslogAppName, + rootConfiguration.SyslogUseTls ); - - var level = rootConfiguration.AppSettings.LoggingLevel; + var level = rootConfiguration.LoggingLevel; if (string.IsNullOrWhiteSpace(level)) { - throw InvalidConfigurationException.For(x => x.AppSettings.LoggingLevel, - "'{prop}' element not found. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); + throw new InvalidConfigurationException( + string.Concat("'{prop}' element not found. Config name: '{0}'", "rootConfiguration.ConfigurationName")); } SetLogLevel(levelSwitch, level); - var logger = loggerConfiguration.CreateLogger(); - return logger; + return loggerConfiguration; } private static void ConfigureLogging( LoggerConfiguration loggerConfiguration, string? loggingFormat, string? fileTemplate, + int? fileSize, string? consoleTemplate) { var adapterPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); @@ -68,14 +67,13 @@ private static void ConfigureLogging( .WriteTo.File(formatter, logsPath, flushToDiskInterval: TimeSpan.FromSeconds(1), - rollingInterval: RollingInterval.Day) ; + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: fileSize) ; if (!string.IsNullOrWhiteSpace(fileTemplate)) { Log.Logger.Warning( - "The {LoggingFormat:l} parameter cannot be used together with the template. The {FileLogOutputTemplate:l} parameter will be ignored.", - RadiusAdapterConfigurationDescription.Property(x => x.AppSettings.LoggingFormat), - RadiusAdapterConfigurationDescription.Property(x => x.AppSettings.FileLogOutputTemplate)); + "The 'logging-format' parameter cannot be used together with the template. The 'file-log-output-template' parameter will be ignored."); } return; @@ -85,7 +83,7 @@ private static void ConfigureLogging( loggerConfiguration.WriteTo.Console(outputTemplate: consoleTemplate); else loggerConfiguration.WriteTo.Console(); - + if (!string.IsNullOrWhiteSpace(fileTemplate)) { loggerConfiguration.WriteTo.File( @@ -170,24 +168,15 @@ private static void ConfigureSyslog( private static void SetLogLevel(LoggingLevelSwitch levelSwitch, string level) { - switch (level) + levelSwitch.MinimumLevel = level switch { - case "Verbose": - levelSwitch.MinimumLevel = LogEventLevel.Verbose; - break; - case "Debug": - levelSwitch.MinimumLevel = LogEventLevel.Debug; - break; - case "Info": - levelSwitch.MinimumLevel = LogEventLevel.Information; - break; - case "Warn": - levelSwitch.MinimumLevel = LogEventLevel.Warning; - break; - case "Error": - levelSwitch.MinimumLevel = LogEventLevel.Error; - break; - } + "Verbose" => LogEventLevel.Verbose, + "Debug" => LogEventLevel.Debug, + "Info" => LogEventLevel.Information, + "Warn" => LogEventLevel.Warning, + "Error" => LogEventLevel.Error, + _ => levelSwitch.MinimumLevel + }; Log.Logger.Information("Logging minimum level: {Level:l}", levelSwitch.MinimumLevel); } diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/StartupLogger.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs similarity index 86% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/StartupLogger.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs index b5849a6a..5ecdedbb 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/StartupLogger.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices.JavaScript; using Serilog; using Serilog.Core; using Serilog.Debugging; @@ -11,7 +12,7 @@ public static class StartupLogger private const string StartupLogFile = "startup.log"; private const long FileSizeLimitBytes = 1024 * 1024 * 20; private const string FileLogTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}|{Level:u3}|{SourceContext:l}] {Message:lj}{NewLine}{Exception}{Properties}{NewLine}"; - private const string ConsoleLogTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}|{Level:u3}|{SourceContext:l}] {Message:lj}{NewLine}{Exception}{Properties}{NewLine}"; + private const string ConsoleLogTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3} {SourceContext:l}] {Message:lj} {Exception}{NewLine}"; private static readonly Lazy _logger = new(() => { @@ -45,10 +46,11 @@ public static class StartupLogger /// public static void Information(string message, params object?[] values) => _logger.Value.Information(message, values); + public static void Warning(string message, params object?[] values) => _logger.Value.Warning(message, values); - /// + /// public static void Error(string message, params object?[] values) => _logger.Value.Error(message, values); - /// + /// public static void Error(Exception ex, string message, params object?[] values) => _logger.Value.Error(ex, message, values); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Multifactor.Radius.Adapter.v2.Infrastructure.csproj b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Multifactor.Radius.Adapter.v2.Infrastructure.csproj new file mode 100644 index 00000000..e8f7cc65 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Multifactor.Radius.Adapter.v2.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusPacketBuilder.cs new file mode 100644 index 00000000..13d96062 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusPacketBuilder.cs @@ -0,0 +1,10 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; + +public interface IRadiusPacketBuilder +{ + byte[] Build(RadiusPacket packet, SharedSecret sharedSecret); + RadiusPacket CreateResponse(RadiusPacket request, PacketCode responseCode); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs new file mode 100644 index 00000000..883de767 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -0,0 +1,237 @@ +using System.Net; +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; + +public class RadiusPacketBuilder : IRadiusPacketBuilder +{ + private readonly IRadiusDictionary _radiusDictionary; + private readonly IRadiusCryptoProvider _cryptoProvider; + + + /// + /// User-Password + /// + public const int UserPassword = 2; + + /// + /// Vendor-Specific + /// + public const int VendorSpecific = 26; + + /// + /// Message-Authenticator + /// + public const int MessageAuthenticator = 80; + + public RadiusPacketBuilder( + IRadiusDictionary radiusDictionary, + IRadiusCryptoProvider cryptoProvider) + { + _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); + _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); + } + + public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) + { + ArgumentNullException.ThrowIfNull(packet); + ArgumentNullException.ThrowIfNull(sharedSecret); + + + var packetBytes = new List + { + // Header: Code (1), Identifier (1), Length (2), Authenticator (16) + (byte)packet.Code, + packet.Identifier + }; + + packetBytes.AddRange(new byte[18]); // Placeholder for length and authenticator + + // Serialize attributes + FillAttributes(packetBytes, packet.Authenticator, sharedSecret, packet.Attributes.Values, out int messageAuthenticatorPosition); + + // Set packet length + ushort packetLength = (ushort)packetBytes.Count; + var lengthBytes = BitConverter.GetBytes(packetLength); + packetBytes[2] = lengthBytes[1]; + packetBytes[3] = lengthBytes[0]; + + var packetBytesArray = packetBytes.ToArray(); + + // Calculate authenticator based on packet type + byte[] authenticator; + switch (packet.Code) + { + case PacketCode.AccountingRequest: + case PacketCode.DisconnectRequest: + case PacketCode.CoaRequest: + if (messageAuthenticatorPosition != 0) + { + FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret); + } + authenticator = _cryptoProvider.CalculateRequestAuthenticator(sharedSecret, packetBytesArray); + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + break; + + case PacketCode.StatusServer: + authenticator = packet.RequestAuthenticator != null + ? _cryptoProvider.CalculateResponseAuthenticator( + sharedSecret, + packet.RequestAuthenticator.Value.ToArray(), + packetBytesArray) + : packet.Authenticator.Value.ToArray(); + + if (messageAuthenticatorPosition != 0) + { + FillMessageAuthenticator( + packetBytesArray, + messageAuthenticatorPosition, + sharedSecret, + packet.RequestAuthenticator); + } + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + + break; + + default: + if (packet.RequestAuthenticator == null) + { + Buffer.BlockCopy(packet.Authenticator.Value, 0, packetBytesArray, 4, 16); + } + + if (messageAuthenticatorPosition != 0) + { + FillMessageAuthenticator( + packetBytesArray, + messageAuthenticatorPosition, + sharedSecret, + packet.RequestAuthenticator); + } + + if (packet.RequestAuthenticator != null) + { + authenticator = _cryptoProvider.CalculateResponseAuthenticator( + sharedSecret, + packet.RequestAuthenticator.Value.ToArray(), + packetBytesArray); + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + } + break; + } + + + return packetBytesArray; + } + + public RadiusPacket CreateResponse(RadiusPacket request, PacketCode responseCode) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + var header = RadiusPacketHeader.Create(responseCode, request.Identifier); + var response = new RadiusPacket(header, requestAuthenticator: request.Authenticator); + + return response; + } + + private void FillAttributes(List packetBytes, RadiusAuthenticator authenticator, SharedSecret sharedSecret, IEnumerable attributes, out int messageAuthenticatorPosition) + { + messageAuthenticatorPosition = 0; + foreach (var attribute in attributes) + { + var attributeValues = attribute.Values; + foreach (var value in attributeValues) + { + var contentBytes = GetAttributeValueBytes(value); + var headerBytes = new byte[2]; + + var attributeType = _radiusDictionary.GetAttribute(attribute.Name); + switch (attributeType) + { + case DictionaryVendorAttribute vendorAttribute: + headerBytes = new byte[8]; + headerBytes[0] = VendorSpecific; // VSA type + + var vendorId = BitConverter.GetBytes(vendorAttribute.VendorId); + Array.Reverse(vendorId); + Buffer.BlockCopy(vendorId, 0, headerBytes, 2, 4); + headerBytes[6] = (byte)vendorAttribute.VendorCode; + headerBytes[7] = (byte)(2 + contentBytes.Length); // length of the vsa part + break; + + case DictionaryAttribute dictionaryAttribute: + headerBytes[0] = attributeType.Code; + + // Encrypt password if this is a User-Password attribute + if (dictionaryAttribute.Code == UserPassword) + { + contentBytes = RadiusPasswordProtector.Encrypt(sharedSecret, authenticator, contentBytes); + } + else if (dictionaryAttribute.Code == MessageAuthenticator) // Remember the position of the message authenticator, because it has to be added after everything else + { + messageAuthenticatorPosition = packetBytes.Count; + } + + break; + default: + throw new InvalidOperationException( + "Unknown attribute {attribute.Key}, check spelling or dictionary"); + } + + headerBytes[1] = (byte)(headerBytes.Length + contentBytes.Length); + packetBytes.AddRange(headerBytes); + packetBytes.AddRange(contentBytes); + } + } + } + + /// + /// Gets the byte representation of an attribute object + /// + /// + /// + private static byte[] GetAttributeValueBytes(object value) + { + switch (value) + { + case string val: + return Encoding.UTF8.GetBytes(val); + case uint val: + var contentBytes = BitConverter.GetBytes(val); + Array.Reverse(contentBytes); + return contentBytes; + case int val: + contentBytes = BitConverter.GetBytes(val); + Array.Reverse(contentBytes); + return contentBytes; + case byte[] val: + return val; + case IPAddress val: + return val.GetAddressBytes(); + default: + throw new NotImplementedException(); + } + } + + private void FillMessageAuthenticator( + byte[] packetBytes, + int position, + SharedSecret sharedSecret, + RadiusAuthenticator? requestAuthenticator = null) + { + + var temp = new byte[16]; + Buffer.BlockCopy(temp, 0, packetBytes, position + 2, 16); + var messageAuthenticator = _cryptoProvider.CalculateMessageAuthenticator( + sharedSecret, + packetBytes, + requestAuthenticator); + + Buffer.BlockCopy(messageAuthenticator, 0, packetBytes, position + 2, 16); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs new file mode 100644 index 00000000..9b4a1d35 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs @@ -0,0 +1,244 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.LangFeatures; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; + +public sealed class RadiusClient : IRadiusClient +{ + private readonly UdpClient _udpClient; + private readonly ConcurrentDictionary _pendingRequests = new(); + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ILogger _logger; + private readonly Timer _cleanupTimer; + private readonly TimeSpan _cleanupInterval = TimeSpan.FromMinutes(1); + + private class PendingRequest + { + public TaskCompletionSource TaskCompletionSource { get; } + public DateTime CreatedAt { get; } + public byte Identifier { get; } + public IPEndPoint RemoteEndpoint { get; } + + public PendingRequest(byte identifier, IPEndPoint remoteEndpoint) + { + TaskCompletionSource = new TaskCompletionSource(); + CreatedAt = DateTime.UtcNow; + Identifier = identifier; + RemoteEndpoint = remoteEndpoint; + } + } + + /// + /// Create a radius client which sends and receives responses on localEndpoint + /// + public RadiusClient(IPEndPoint localEndpoint, ILogger logger) + { + Throw.IfNull(localEndpoint); + Throw.IfNull(logger); + + _logger = logger; + _udpClient = new UdpClient(localEndpoint); + _cancellationTokenSource = new CancellationTokenSource(); + + // Запускаем периодическую очистку старых запросов + _cleanupTimer = new Timer(CleanupOldRequests, null, _cleanupInterval, _cleanupInterval); + + // Запускаем цикл приема пакетов + _ = StartReceiveLoopAsync(_cancellationTokenSource.Token); + } + + /// + /// Send a packet with specified timeout + /// + public async Task SendPacketAsync(byte identifier, byte[] requestPacket, IPEndPoint remoteEndpoint, TimeSpan timeout) + { + var key = CreateRequestKey(identifier, remoteEndpoint); + var pendingRequest = new PendingRequest(identifier, remoteEndpoint); + + var timeoutCancellation = new CancellationTokenSource(timeout); + timeoutCancellation.Token.Register(() => + { + if (_pendingRequests.TryRemove(key, out var request)) + { + request.TaskCompletionSource.TrySetCanceled(); + _logger.LogDebug("Request timeout for identifier {identifier} to {remoteEndpoint}", + identifier, remoteEndpoint); + } + }, useSynchronizationContext: false); + + try + { + if (_pendingRequests.TryAdd(key, pendingRequest)) + { + await _udpClient.SendAsync(requestPacket, remoteEndpoint, timeoutCancellation.Token); + return await pendingRequest.TaskCompletionSource.Task; + } + else + { + _logger.LogWarning("Duplicate request detected for identifier {identifier} to {remoteEndpoint}", + identifier, remoteEndpoint); + return null; + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogError(ex, "Error sending packet to {remoteEndpoint}", remoteEndpoint); + _pendingRequests.TryRemove(key, out _); + pendingRequest.TaskCompletionSource.TrySetException(ex); + return null; + } + finally + { + timeoutCancellation.Dispose(); + + _pendingRequests.TryRemove(key, out _); + } + } + + /// + /// Receive packets in a loop and complete tasks based on identifier + /// + private async Task StartReceiveLoopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Starting receive loop"); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + var response = await _udpClient.ReceiveAsync(cancellationToken); + ProcessReceivedPacket(response); + } + catch (ObjectDisposedException) + { + // This is thrown when udpclient is disposed, can be safely ignored + break; + } + catch (OperationCanceledException) + { + // Cancellation requested + break; + } + catch (SocketException ex) + { + _logger.LogError(ex, "Socket error in receive loop"); + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in receive loop"); + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + } + } + + _logger.LogDebug("Receive loop stopped"); + } + + /// + /// Process received UDP packet + /// + private void ProcessReceivedPacket(UdpReceiveResult result) + { + try + { + if (result.Buffer.Length < 2) + { + _logger.LogDebug("Received packet too small: {length} bytes", result.Buffer.Length); + return; + } + + var identifier = result.Buffer[1]; + var key = CreateRequestKey(identifier, result.RemoteEndPoint); + + if (_pendingRequests.TryRemove(key, out var pendingRequest)) + { + pendingRequest.TaskCompletionSource.TrySetResult(result.Buffer); + _logger.LogDebug("Received response for identifier {identifier} from {remoteEndpoint}", + identifier, result.RemoteEndPoint); + } + else + { + _logger.LogDebug("Received unexpected response for identifier {identifier} from {remoteEndpoint}", + identifier, result.RemoteEndPoint); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing received packet from {remoteEndpoint}", + result.RemoteEndPoint); + } + } + + /// + /// Cleanup old pending requests that haven't received responses + /// + private void CleanupOldRequests(object? state) + { + try + { + var cutoffTime = DateTime.UtcNow - TimeSpan.FromMinutes(5); + var removedCount = 0; + + foreach (var kvp in _pendingRequests) + { + if (kvp.Value.CreatedAt < cutoffTime) + { + if (_pendingRequests.TryRemove(kvp.Key, out var request)) + { + request.TaskCompletionSource.TrySetCanceled(); + removedCount++; + } + } + } + + if (removedCount > 0) + { + _logger.LogDebug("Cleaned up {count} old pending requests", removedCount); + } + + _logger.LogTrace("Pending requests count: {count}", _pendingRequests.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during cleanup of pending requests"); + } + } + + /// + /// Create a unique key for a request + /// + private static string CreateRequestKey(byte identifier, IPEndPoint remoteEndpoint) + { + return $"{identifier}_{remoteEndpoint.Address}:{remoteEndpoint.Port}"; + } + + public void Dispose() + { + try + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource?.Dispose(); + + _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); + _cleanupTimer.Dispose(); + + foreach (var kvp in _pendingRequests) + { + kvp.Value.TaskCompletionSource.TrySetCanceled(); + } + _pendingRequests.Clear(); + + _udpClient.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during disposal"); + } + + _logger.LogDebug("RadiusClient disposed"); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClientFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClientFactory.cs similarity index 77% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClientFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClientFactory.cs index f53a21a4..765a4c39 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClientFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClientFactory.cs @@ -1,7 +1,8 @@ using System.Net; using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; -namespace Multifactor.Radius.Adapter.v2.Services.Radius; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; public class RadiusClientFactory : IRadiusClientFactory { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs new file mode 100644 index 00000000..5680e4aa --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs @@ -0,0 +1,12 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +public interface IRadiusCryptoProvider +{ + byte[] CalculateRequestAuthenticator(SharedSecret secret, byte[] packet); + byte[] CalculateResponseAuthenticator(SharedSecret secret, byte[] requestAuth, byte[] responsePacket); + byte[] CalculateMessageAuthenticator(SharedSecret secret, byte[] packet, RadiusAuthenticator? requestAuth = null); + bool ValidateMessageAuthenticator(byte[] packet, byte[] messageAuth, int position, SharedSecret secret, RadiusAuthenticator? requestAuth = null); + string DecryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs new file mode 100644 index 00000000..ac27cd41 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs @@ -0,0 +1,71 @@ +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +public class RadiusCryptoProvider : IRadiusCryptoProvider +{ + private readonly ILogger _logger; + + public RadiusCryptoProvider(ILogger logger) + { + _logger = logger; + } + + public byte[] CalculateRequestAuthenticator(SharedSecret secret, byte[] packet) + { + return CalculateAuthenticator(secret, packet, new byte[16]); + } + + public byte[] CalculateResponseAuthenticator(SharedSecret secret, byte[] requestAuth, byte[] responsePacket) + { + return CalculateAuthenticator(secret, responsePacket, requestAuth); + } + + public byte[] CalculateMessageAuthenticator(SharedSecret secret, byte[] packet, RadiusAuthenticator? requestAuth = null) + { + var temp = new byte[packet.Length]; + packet.CopyTo(temp, 0); + + requestAuth?.Value.CopyTo(temp, 4); + + using var md5 = new HMACMD5(secret.Bytes); + return md5.ComputeHash(temp); + } + + public bool ValidateMessageAuthenticator( + byte[] packet, + byte[] messageAuth, + int position, + SharedSecret secret, + RadiusAuthenticator? requestAuth = null) + { + var tempPacket = new byte[packet.Length]; + packet.CopyTo(tempPacket, 0); + + // Replace the Message-Authenticator content only. + // messageAuthenticatorPosition is a position of the Message-Authenticator block. + // The full-block length is 18: typecode (1), length (1), content (16). + // So the Message-Authenticator content position is (messageAuthenticatorPosition + 2). + Buffer.BlockCopy(new byte[16], 0, tempPacket, position + 2, 16); + + var calculatedMessageAuthenticator = + CalculateMessageAuthenticator(secret, tempPacket, requestAuth); + return calculatedMessageAuthenticator.SequenceEqual(messageAuth); + } + + public string DecryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword) + { + return RadiusPasswordProtector.Decrypt(secret, authenticator, encryptedPassword); + } + + private static byte[] CalculateAuthenticator(SharedSecret secret, byte[] packet, byte[] requestAuth) + { + var responseAuthenticator = packet.Concat(secret.Bytes).ToArray(); + Buffer.BlockCopy(requestAuth, 0, responseAuthenticator, 4, 16); + + using var md5 = MD5.Create(); + return md5.ComputeHash(responseAuthenticator); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPasswordProtector.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs index 323c921e..6ce16ca1 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPasswordProtector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs @@ -1,9 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using System.Security.Cryptography; using System.Text; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -namespace Multifactor.Radius.Adapter.v2.Services.Radius +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto { public static class RadiusPasswordProtector { @@ -94,4 +93,4 @@ public static byte[] Encrypt(SharedSecret sharedSecret, RadiusAuthenticator auth return bytes.ToArray(); } } -} +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs new file mode 100644 index 00000000..2b04c994 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; + +public interface IRadiusAttributeParser +{ + public ParsedAttribute? Parse(byte[] attributeData, byte typeCode, RadiusAuthenticator authenticator, + SharedSecret sharedSecret); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusPacketParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusPacketParser.cs new file mode 100644 index 00000000..07a9492b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusPacketParser.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; + +public interface IRadiusPacketParser +{ + RadiusPacket Parse(byte[] packetBytes, SharedSecret sharedSecret); + RadiusPacket Parse(byte[] packetBytes, SharedSecret sharedSecret, RadiusAuthenticator requestAuthenticator); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs new file mode 100644 index 00000000..6c9d5122 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -0,0 +1,154 @@ +using System.Net; +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; + +public class RadiusAttributeParser : IRadiusAttributeParser +{ + private readonly IRadiusDictionary _radiusDictionary; + private readonly ILogger _logger; + const int VendorSpecific = 26; + const int MessageAuthenticator = 80; + const int UserPassword = 2; + + public RadiusAttributeParser( + IRadiusDictionary radiusDictionary, + ILogger logger) + { + _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public ParsedAttribute? Parse(byte[] attributeData, byte typeCode, RadiusAuthenticator authenticator, SharedSecret sharedSecret) + { + try + { + if (typeCode == VendorSpecific) // Vendor-Specific + { + return ParseVendorSpecificAttribute(attributeData, authenticator, sharedSecret); + } + return ParseStandardAttribute(typeCode, attributeData, authenticator, sharedSecret); + + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to parse attribute type {TypeCode}", typeCode); + return null; + } + } + + private ParsedAttribute? ParseVendorSpecificAttribute( + byte[] contentBytes, + RadiusAuthenticator authenticator, + SharedSecret sharedSecret) + { + if (contentBytes.Length < 6) + return null; + + byte[] vendorIdBytes = new byte[4]; + Buffer.BlockCopy(contentBytes, 0, vendorIdBytes, 0, 4); + Array.Reverse(vendorIdBytes); + uint vendorId = BitConverter.ToUInt32(vendorIdBytes, 0); + + byte vendorType = contentBytes[4]; + byte vendorLength = contentBytes[5]; + + if (vendorLength < 2 || vendorLength > contentBytes.Length) + return null; + + byte[] vendorContentBytes = new byte[vendorLength - 2]; + Buffer.BlockCopy(contentBytes, 6, vendorContentBytes, 0, vendorContentBytes.Length); + + var vendorAttribute = _radiusDictionary.GetVendorAttribute(vendorId, vendorType); + if (vendorAttribute == null) + { + _logger.LogDebug("Unknown VSA: VendorId={VendorId}, VendorType={VendorType}", vendorId, vendorType); + return null; + } + + var content = ParseContentBytes( + vendorContentBytes, + vendorAttribute.Type, + 26, + authenticator, + sharedSecret); + + if (content == null) + return null; + + return new ParsedAttribute(vendorAttribute.Name, content, false); + } + + private ParsedAttribute? ParseStandardAttribute( + byte typeCode, + byte[] contentBytes, + RadiusAuthenticator authenticator, + SharedSecret sharedSecret) + { + var attributeDefinition = _radiusDictionary.GetAttribute(typeCode); + if (attributeDefinition == null) + { + _logger.LogDebug("Unknown attribute type: {TypeCode}", typeCode); + return null; + } + + var content = ParseContentBytes( + contentBytes, + attributeDefinition.Type, + typeCode, + authenticator, + sharedSecret); + + if (content == null) + return null; + + bool isMessageAuthenticator = attributeDefinition.Code == MessageAuthenticator; + + return new ParsedAttribute(attributeDefinition.Name, content, isMessageAuthenticator); + } + + private static object? ParseContentBytes( + byte[] contentBytes, + string type, + uint code, + RadiusAuthenticator authenticator, + SharedSecret sharedSecret) + { + switch (type) + { + case DictionaryAttribute.TypeTaggedString: + case DictionaryAttribute.TypeString: + //couse some NAS (like NPS) send binary within string attributes, check content before unpack to prevent data loss + if (contentBytes.All(b => b >= 32 && b <= 127)) //only if ascii + { + return Encoding.UTF8.GetString(contentBytes); + } + + return contentBytes; + + case DictionaryAttribute.TypeOctet: + // If this is a password attribute it must be decrypted + if (code == UserPassword) + { + return RadiusPasswordProtector.Decrypt(sharedSecret, authenticator, contentBytes); + } + + return contentBytes; + + case DictionaryAttribute.TypeInteger: + case DictionaryAttribute.TypeTaggedInteger: + return BitConverter.ToInt32(contentBytes.Reverse().ToArray(), 0); + + case DictionaryAttribute.TypeIpAddr: + return new IPAddress(contentBytes); + + default: + return null; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs new file mode 100644 index 00000000..a9e6baa6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs @@ -0,0 +1,166 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; + +public class RadiusPacketParser : IRadiusPacketParser +{ + private readonly IRadiusAttributeParser _attributeParser; + private readonly IRadiusCryptoProvider _cryptoProvider; + private readonly ILogger _logger; + + public const int LengthFieldPosition = 2; + public const int LengthFieldLength = 2; + + public const int AttributesFieldPosition = 20; + public RadiusPacketParser( + IRadiusAttributeParser attributeParser, + IRadiusCryptoProvider cryptoProvider, + ILogger logger) + { + _attributeParser = attributeParser ?? throw new ArgumentNullException(nameof(attributeParser)); + _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public RadiusPacket Parse(byte[] packetBytes, SharedSecret sharedSecret) + { + return ParseInternal(packetBytes, sharedSecret, null); + } + + public RadiusPacket Parse(byte[] packetBytes, SharedSecret sharedSecret, RadiusAuthenticator requestAuthenticator) + { + return ParseInternal(packetBytes, sharedSecret, requestAuthenticator); + } + + private RadiusPacket ParseInternal( + byte[] packetBytes, + SharedSecret sharedSecret, + RadiusAuthenticator? requestAuthenticator) + { + ValidatePacketLength(packetBytes); + ValidatePacketLengthField(packetBytes); + + var header = RadiusPacketHeader.Parse(packetBytes); + var packet = new RadiusPacket(header, requestAuthenticator); + + if (packet.Code == PacketCode.AccountingRequest || packet.Code == PacketCode.DisconnectRequest) + { + var requestAuth = _cryptoProvider.CalculateRequestAuthenticator(sharedSecret, packetBytes); + if (!packet.Authenticator.Value.SequenceEqual(requestAuth)) + { + throw new InvalidOperationException( + $"Invalid request authenticator in packet {packet.Identifier}, check secret?"); + } + } + + ParseAttributes(packetBytes, packet, sharedSecret); + + return packet; + } + + + private static ushort GetPacketLength(byte[] packetBytes) + { + var packetLengthbytes = new byte[LengthFieldLength]; + // Length field always third and fourth bytes in packet (rfc2865) + packetLengthbytes[0] = packetBytes[LengthFieldPosition + 1]; + packetLengthbytes[1] = packetBytes[LengthFieldPosition]; + var packetLength = BitConverter.ToUInt16(packetLengthbytes, 0); + return packetLength; + } + + private static void ValidatePacketLength(byte[] packetBytes) + { + if (packetBytes.Length < 20) + { + throw new InvalidOperationException($"Packet too short: {packetBytes.Length} bytes. Minimum is 20 bytes."); + } + } + + private static void ValidatePacketLengthField(byte[] packetBytes) + { + var declaredLength = BitConverter.ToUInt16([packetBytes[3], packetBytes[2]], 0); + + if (declaredLength != packetBytes.Length) + { + throw new InvalidOperationException( + $"Packet length mismatch. Declared: {declaredLength}, Actual: {packetBytes.Length}"); + } + + if (declaredLength > 4096) + { + throw new InvalidOperationException( + $"Packet too large: {declaredLength} bytes. Maximum is 4096 bytes."); + } + } + + private void ParseAttributes( + byte[] packetBytes, + RadiusPacket packet, + SharedSecret sharedSecret) + { + int position = AttributesFieldPosition; + int messageAuthenticatorPosition = 0; + + ushort packetLength = GetPacketLength(packetBytes); + + while (position < packetBytes.Length) + { + var typeCode = packetBytes[position]; + var length = packetBytes[position + 1]; + + if (position + length > packetLength) + { + throw new ArgumentOutOfRangeException(); + } + + var attributeData = new byte[length - 2]; + Buffer.BlockCopy(packetBytes, position + 2, attributeData, 0, length - 2); + + try + { + + var parsedAttribute = _attributeParser.Parse(attributeData, typeCode, packet.Authenticator, sharedSecret); + + if (parsedAttribute != null) + { + packet.AddAttributeValue(parsedAttribute.Name, parsedAttribute.Value); + if (parsedAttribute.IsMessageAuthenticator) + { + messageAuthenticatorPosition = position; + } + } + } + catch (KeyNotFoundException) + { + _logger.LogWarning("Attribute {typecode:l} not found in dictionary", typeCode); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to parse attribute type {TypeCode} at position {Position}", + typeCode, position); + } + + position += length; + } + + if (messageAuthenticatorPosition != 0) + { + var messageAuthenticator = packet.GetAttribute("Message-Authenticator"); + var isValid = _cryptoProvider.ValidateMessageAuthenticator( + packetBytes, + messageAuthenticator, + messageAuthenticatorPosition, + sharedSecret, + packet.RequestAuthenticator); + + if (!isValid) + { + throw new InvalidOperationException("Invalid Message-Authenticator"); + } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs new file mode 100644 index 00000000..316585da --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -0,0 +1,273 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; + +public class AdapterResponseSender : IResponseSender +{ + private readonly IRadiusPacketService _radiusPacketService; + private readonly IRadiusReplyAttributeService _radiusReplyAttributeService; + private readonly IUdpClient _udpClient; + private readonly ILogger _logger; + + private const string MessageAuthenticatorAttribute = "Message-Authenticator"; + private const string ProxyStateAttribute = "Proxy-State"; + private const string StateAttribute = "State"; + private const string ReplyMessageAttribute = "Reply-Message"; + + public AdapterResponseSender( + IRadiusPacketService radiusPacketService, + IUdpClient udpClient, + IRadiusReplyAttributeService radiusReplyAttributeService, + ILogger logger) + { + _radiusPacketService = radiusPacketService ?? throw new ArgumentNullException(nameof(radiusPacketService)); + _udpClient = udpClient ?? throw new ArgumentNullException(nameof(udpClient)); + _radiusReplyAttributeService = radiusReplyAttributeService ?? throw new ArgumentNullException(nameof(radiusReplyAttributeService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task SendResponse(SendAdapterResponseRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (request.ShouldSkipResponse) + { + _logger.LogDebug("Skipping response for request Id={Id}", request.RequestPacket?.Identifier); + return; + } + + if (request.ResponsePacket?.IsEapMessageChallenge == true) + { + // EAP challenge + _logger.LogDebug("Proxying EAP-Message Challenge to {host:l}:{port} id={id}", request.RemoteEndpoint.Address, request.RemoteEndpoint.Port, request.RequestPacket.Identifier); + await SendResponsePacketAsync(request.ResponsePacket, request); + return; + } + + // Vendor ACL request + if (request.RequestPacket.IsVendorAclRequest && request.ResponsePacket != null) + { + //ACL and other rules transfer, just proxy response + _logger.LogDebug("Proxying #ACSACL# to {host:l}:{port} id={id}", request.RemoteEndpoint.Address, request.RemoteEndpoint.Port, request.RequestPacket.Identifier); + await SendResponsePacketAsync(request.ResponsePacket, request); + return; + } + + + var responsePacket = BuildResponsePacket(request); + + await SendResponsePacketAsync(responsePacket, request); + + LogResponseSent(responsePacket, request); + } + + private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) + { + var responsePacketCode = DetermineResponseCode(request.FirstFactorStatus, request.SecondFactorStatus); + var responsePacket = _radiusPacketService.CreateResponse( + request.RequestPacket, + responsePacketCode); + + switch (responsePacketCode) + { + case PacketCode.AccessAccept: + ProcessAccessAcceptResponse(responsePacket, request); + break; + + case PacketCode.AccessReject: + ProcessAccessRejectResponse(responsePacket, request); + break; + + case PacketCode.AccessChallenge: + ProcessAccessChallengeResponse(responsePacket, request); + break; + + default: + throw new NotSupportedException( + $"Response packet code {responsePacketCode} is not supported"); + } + + AddCommonAttributes(responsePacket, request); + + return responsePacket; + } + + private void ProcessAccessAcceptResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + if (request.ResponsePacket != null) + { + CopyAttributes(request.ResponsePacket, responsePacket); + } + + AddReplyAttributes(responsePacket, request); + } + + private void ProcessAccessRejectResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + if (request.ResponsePacket?.Code == PacketCode.AccessReject) + { + CopyAttributes(request.ResponsePacket, responsePacket); + } + } + + private static void ProcessAccessChallengeResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + if (!string.IsNullOrWhiteSpace(request.ResponseInformation.State)) + { + responsePacket.ReplaceAttribute(StateAttribute, request.ResponseInformation.State); + } + } + + private void AddCommonAttributes(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + // Reply-Message + if (!string.IsNullOrWhiteSpace(request.ResponseInformation.ReplyMessage)) + { + responsePacket.ReplaceAttribute(ReplyMessageAttribute, request.ResponseInformation.ReplyMessage); + } + + // Proxy-State + AddProxyStateAttribute(request.RequestPacket, responsePacket); + + // Message-Authenticator (placeholder если нет) + AddMessageAuthenticatorIfMissing(responsePacket); + } + + private static void CopyAttributes(RadiusPacket source, RadiusPacket target) + { + if (source == null || target == null) + return; + + foreach (var attribute in source.Attributes.Values) + { + target.RemoveAttribute(attribute.Name); + + foreach (var value in attribute.Values) + { + target.AddAttributeValue(attribute.Name, value); + } + } + } + + private static void AddProxyStateAttribute(RadiusPacket source, RadiusPacket target) + { + if (source.Attributes.TryGetValue(ProxyStateAttribute, out var proxyStateAttribute)) + { + if (!target.Attributes.ContainsKey(ProxyStateAttribute)) + { + var value = proxyStateAttribute.Values.FirstOrDefault(); + if (value != null) + { + target.AddAttributeValue(ProxyStateAttribute, value); + } + } + } + } + + private static void AddMessageAuthenticatorIfMissing(RadiusPacket packet) + { + if (!packet.Attributes.ContainsKey(MessageAuthenticatorAttribute)) + { + var placeholder = new byte[16]; + var placeholderStr = Encoding.ASCII.GetString(placeholder); + packet.AddAttributeValue(MessageAuthenticatorAttribute, placeholderStr); + } + } + + private void AddReplyAttributes(RadiusPacket target, SendAdapterResponseRequest request) + { + var replyAttributesRequest = new GetReplyAttributesRequest( + request.RequestPacket.UserName, + request.UserGroups, + request.RadiusReplyAttributes, + request.Attributes); + + var attributes = _radiusReplyAttributeService.GetReplyAttributes(replyAttributesRequest); + + foreach (var attribute in attributes) + { + target.RemoveAttribute(attribute.Key); + + foreach (var attrValue in attribute.Value) + { + target.AddAttributeValue(attribute.Key, attrValue); + } + } + } + + private static PacketCode DetermineResponseCode(AuthenticationStatus firstFactorStatus, AuthenticationStatus secondFactorStatus) + { + var successfulFirstFactor = firstFactorStatus + is AuthenticationStatus.Accept + or AuthenticationStatus.Bypass; + + var successfulSecondFactor = secondFactorStatus + is AuthenticationStatus.Accept + or AuthenticationStatus.Bypass; + + if (successfulFirstFactor && successfulSecondFactor) + return PacketCode.AccessAccept; + + var authFailed = firstFactorStatus == AuthenticationStatus.Reject + || secondFactorStatus == AuthenticationStatus.Reject; + + return authFailed ? PacketCode.AccessReject : PacketCode.AccessChallenge; + } + + private async Task SendResponsePacketAsync(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + var bytes = _radiusPacketService.SerializePacket(responsePacket, request.RadiusSharedSecret); + var endpoint = request.ProxyEndpoint ?? request.RemoteEndpoint; + + // Задержка для AccessReject (security feature) + if (responsePacket.Code == PacketCode.AccessReject + && request.InvalidCredentialDelay != null) + { + await WaitSomeTimeAsync( + request.InvalidCredentialDelay.Min, + request.InvalidCredentialDelay.Max); + } + + await _udpClient.SendAsync(bytes, bytes.Length, endpoint); + } + private void LogResponseSent(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + var endpoint = request.ProxyEndpoint ?? request.RemoteEndpoint; + var userName = request.RequestPacket.UserName; + + if (!string.IsNullOrWhiteSpace(userName)) + { + _logger.LogInformation( + "{Code} sent to {Host}:{Port} id={Id} user='{User}'", + responsePacket.Code.ToString(), + endpoint.Address, + endpoint.Port, + responsePacket.Identifier, + userName); + } + else + { + _logger.LogInformation( + "{Code} sent to {Host}:{Port} id={Id}", + responsePacket.Code.ToString(), + endpoint.Address, + endpoint.Port, + responsePacket.Identifier); + } + } + + private static Task WaitSomeTimeAsync(int min, int max) + { + var correctedMax = min == max ? max : max + 1; + var delay = new Random().Next(min, correctedMax); + + return Task.Delay(TimeSpan.FromSeconds(delay)); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs new file mode 100644 index 00000000..0c1d77b5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; + +public class RadiusAttributeTypeConverter : IRadiusAttributeTypeConverter +{ + private readonly IRadiusDictionary _radiusDictionary; + + public RadiusAttributeTypeConverter(IRadiusDictionary radiusDictionary) + { + _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); + } + + public object ConvertType(string attributeName, object value) + { + if (value is not string stringValue) + return value; + + var attributeInfo = _radiusDictionary.GetAttribute(attributeName); + if (attributeInfo == null) + { + return value; + } + + return ConvertStringToType(stringValue, attributeInfo.Type); + } + + private object ConvertStringToType(string stringValue, string attributeType) + { + return attributeType.ToLowerInvariant() switch + { + "ipaddr" => ConvertToIpAddress(stringValue), + "date" => ConvertToDateTime(stringValue), + "integer" => ConvertToInteger(stringValue), + "string" or "tagged-string" => stringValue, + "octets" => ConvertToOctets(stringValue), + _ => stringValue + }; + } + + private object ConvertToIpAddress(string stringValue) + { + if (IPAddress.TryParse(stringValue, out var ipAddress)) + return ipAddress; + + if (int.TryParse(stringValue, out var intValue)) + return ConvertMsRadiusFramedIpAddress(intValue); + + return stringValue; + } + + private static IPAddress ConvertMsRadiusFramedIpAddress(int intValue) + { + // Microsoft RADIUS специфика: + // Числа выше 2147483647 представляются как отрицательные + long longValue = intValue; + + if (longValue < 0) + { + // Конвертируем negative int в unsigned long + longValue += 4294967296L; // 2^32 + } + + // Конвертируем в байты (big-endian для IP-адреса) + var bytes = BitConverter.GetBytes(longValue); + + // Берем только первые 4 байта (IPv4) + var ipBytes = new byte[4]; + Array.Copy(bytes, 0, ipBytes, 0, 4); + + // Конвертируем big-endian если нужно + if (BitConverter.IsLittleEndian) + { + Array.Reverse(ipBytes); + } + + return new IPAddress(ipBytes); + } + + private static object ConvertToDateTime(string stringValue) + { + if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var dateTime)) + { + return dateTime; + } + + if (long.TryParse(stringValue, out var unixTimestamp)) + { + return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime; + } + + return stringValue; + } + + private static object ConvertToInteger(string stringValue) + { + if (int.TryParse(stringValue, out var intValue)) + return intValue; + + return stringValue; + } + + private static object ConvertToOctets(string stringValue) + { + // Для octets можно конвертировать из hex или base64 + try + { + // Пробуем как hex строку + if (stringValue.Length % 2 == 0 && + stringValue.All(c => char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) + { + return HexStringToByteArray(stringValue); + } + + // Пробуем как base64 + if (stringValue.Length % 4 == 0 && + stringValue.All(c => char.IsLetterOrDigit(c) || c == '+' || c == '/' || c == '=')) + { + return Convert.FromBase64String(stringValue); + } + } + catch + { + // Если не удалось - возвращаем как строку + } + + return stringValue; + } + + private static byte[] HexStringToByteArray(string hex) + { + var bytes = new byte[hex.Length / 2]; + for (int i = 0; i < hex.Length; i += 2) + { + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + } + return bytes; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusNasIdentifierExtractor.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusNasIdentifierExtractor.cs new file mode 100644 index 00000000..c2c249b7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusNasIdentifierExtractor.cs @@ -0,0 +1,63 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; + + +public interface INasIdentifierExtractor +{ + bool TryExtract(byte[] packetBytes, out string nasIdentifier); +} + +public class RadiusNasIdentifierExtractor : INasIdentifierExtractor +{ + private const int NasIdentifierAttributeCode = 32; + private const int MinimumPacketLength = 20; + + public bool TryExtract(byte[] packetBytes, out string nasIdentifier) + { + nasIdentifier = string.Empty; + + if (packetBytes == null || packetBytes.Length < MinimumPacketLength) + return false; + + try + { + // Read packet length from bytes 2-3 (network byte order) + ushort packetLength = BitConverter.ToUInt16(new[] { packetBytes[3], packetBytes[2] }, 0); + + if (packetBytes.Length != packetLength) + return false; + + int position = 20; // Start of attributes + + while (position < packetBytes.Length) + { + if (position + 1 >= packetBytes.Length) + break; + + byte typeCode = packetBytes[position]; + byte length = packetBytes[position + 1]; + + if (length < 2 || position + length > packetBytes.Length) + break; + + if (typeCode == NasIdentifierAttributeCode) + { + byte[] contentBytes = new byte[length - 2]; + Buffer.BlockCopy(packetBytes, position + 2, contentBytes, 0, contentBytes.Length); + + nasIdentifier = Encoding.UTF8.GetString(contentBytes).TrimEnd('\0'); + return !string.IsNullOrEmpty(nasIdentifier); + } + + position += length; + } + + return false; + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs new file mode 100644 index 00000000..bc2825fa --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; + +public class RadiusPacketService : IRadiusPacketService +{ + private readonly IRadiusPacketParser _parser; + private readonly IRadiusPacketBuilder _builder; + private readonly IRadiusPacketValidator _validator; + private readonly ILogger _logger; + private readonly INasIdentifierExtractor _nasIdentifierExtractor; + + public RadiusPacketService( + IRadiusPacketParser parser, + IRadiusPacketBuilder builder, + IRadiusPacketValidator validator, + INasIdentifierExtractor nasIdentifierExtractor, + ILogger logger) + { + _parser = parser; + _builder = builder; + _validator = validator; + _nasIdentifierExtractor = nasIdentifierExtractor; + _logger = logger; + } + + public RadiusPacket ParsePacket(byte[] packetBytes, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null) + { + if (packetBytes == null) throw new ArgumentNullException(nameof(packetBytes)); + if (sharedSecret == null) throw new ArgumentNullException(nameof(sharedSecret)); + + try + { + _logger.LogDebug("Parsing RADIUS packet, length: {Length}", packetBytes.Length); + + _validator.ValidateRawPacket(packetBytes); + + var packet = requestAuthenticator == null ? _parser.Parse(packetBytes, sharedSecret) + : _parser.Parse(packetBytes, sharedSecret, requestAuthenticator); + + _validator.ValidateParsedPacket(packet, sharedSecret); + + _logger.LogDebug("Successfully parsed RADIUS packet: Code={Code}, Id={Id}, Attributes={AttributeCount}", + packet.Code, packet.Identifier, packet.Attributes.Count); + + return packet; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to parse RADIUS packet. Length: {Length}", packetBytes.Length); + throw new RadiusPacketException("Failed to parse RADIUS packet", ex); + } + } + + public byte[] SerializePacket(RadiusPacket packet, SharedSecret sharedSecret) + { + ArgumentNullException.ThrowIfNull(packet); + ArgumentNullException.ThrowIfNull(sharedSecret); + + try + { + _logger.LogDebug("Serializing RADIUS packet: Code={Code}, Id={Id}", packet.Code, packet.Identifier); + + _validator.ValidatePacketForSerialization(packet); + + var result = _builder.Build(packet, sharedSecret); + + _logger.LogDebug("Successfully serialized RADIUS packet, length: {Length}", result.Length); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to serialize RADIUS packet: Code={Code}, Id={Id}", + packet.Code, packet.Identifier); + throw new RadiusPacketException("Failed to serialize RADIUS packet", ex); + } + } + + + public RadiusPacket CreateResponse(RadiusPacket request, PacketCode responseCode) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + try + { + _logger.LogDebug("Creating response packet for request Id={Id}, ResponseCode={ResponseCode}", + request.Identifier, responseCode); + + var response = _builder.CreateResponse(request, responseCode); + + _logger.LogDebug("Successfully created response packet: Code={Code}, Id={Id}", + response.Code, response.Identifier); + + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create response packet for request Id={Id}", request.Identifier); + throw new RadiusPacketException("Failed to create response packet", ex); + } + } + + public bool TryGetNasIdentifier(byte[] packetBytes, out string nasIdentifier) + { + if (packetBytes == null) throw new ArgumentNullException(nameof(packetBytes)); + + return _nasIdentifierExtractor.TryExtract(packetBytes, out nasIdentifier); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs new file mode 100644 index 00000000..cc1f702d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -0,0 +1,187 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; + +public class RadiusReplyAttributeService : IRadiusReplyAttributeService +{ + private readonly IRadiusAttributeTypeConverter _typeConverter; + private readonly ILogger _logger; + + public RadiusReplyAttributeService( + IRadiusAttributeTypeConverter typeConverter, + ILogger logger) + { + _typeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public IDictionary> GetReplyAttributes(GetReplyAttributesRequest request) + { + ArgumentNullException.ThrowIfNull(request, nameof(request)); + ArgumentNullException.ThrowIfNull(request.ReplyAttributes, nameof(request.ReplyAttributes)); + + var result = new Dictionary>(); + + foreach (var attribute in request.ReplyAttributes) + { + var values = ProcessAttribute(attribute.Key, attribute.Value, request); + if (values.Count != 0) + { + result[attribute.Key] = values; + } + + if (IsSufficientAttribute(attribute.Value)) + { + _logger.LogDebug("Sufficient attribute '{Attribute}' found, stopping processing", attribute.Key); + break; + } + } + + LogResult(result); + return result; + } + + private List ProcessAttribute( + string attributeName, + IReadOnlyList attributeValues, + GetReplyAttributesRequest request) + { + var result = new List(); + + foreach (var attributeValue in attributeValues) + { + if (!ShouldIncludeAttribute(attributeValue, request)) + continue; + + var values = GetAttributeValues(attributeValue, request); + foreach (var value in values) + { + if (value is null) + { + _logger.LogDebug("Skipping null value for attribute '{Attribute}'", attributeName); + continue; + } + + var convertedValue = _typeConverter.ConvertType(attributeName, value); + result.Add(convertedValue); + + _logger.LogDebug( + "Added attribute '{Attribute}': {Value}", + attributeName, + GetLoggableValue(convertedValue)); + } + + if (attributeValue.Sufficient) + break; + } + + return result; + } + + private bool ShouldIncludeAttribute(IRadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + { + if (attributeValue.FromLdap) + { + if (attributeValue.IsMemberOf) + return request.UserGroups?.Count > 0; + + return !string.IsNullOrEmpty(attributeValue.Name) && + request.HasAttribute(attributeValue.Name); + } + + if (attributeValue.UserNameCondition.Count > 0) + { + return MatchesUserNameCondition(attributeValue.UserNameCondition, request.UserName); + } + + if (attributeValue.UserGroupCondition.Count > 0) + { + return MatchesUserGroupCondition(attributeValue.UserGroupCondition, request.UserGroups); + } + + return true; + } + + private static bool MatchesUserNameCondition(IReadOnlyList conditions, string? userName) + { + if (string.IsNullOrWhiteSpace(userName)) + return false; + + var canonicalUserName = userName.CanonicalizeUserName(); + + foreach (var condition in conditions) + { + var nameToMatch = condition.IsCanonicalUserName() + ? canonicalUserName + : userName; + + if (string.Equals(nameToMatch, condition, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private static bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet? userGroups) + { + if (userGroups == null || userGroups.Count == 0) + return false; + + return conditions + .Any(condition => userGroups + .Any(group => string.Equals(group, condition, StringComparison.OrdinalIgnoreCase))); + } + + private static List GetAttributeValues(IRadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + { + if (attributeValue.IsMemberOf) + { + return request.UserGroups + .Select(group => (object?)group) + .ToList(); + } + + if (attributeValue.FromLdap && !string.IsNullOrEmpty(attributeValue.Name)) + { + return request.GetAttributeValues(attributeValue.Name) + .Select(value => (object?)value) + .ToList(); + } + + return [attributeValue.Value]; + } + + private static bool IsSufficientAttribute(IReadOnlyList attributeValues) + { + return attributeValues.Any(av => av.Sufficient); + } + + private void LogResult(IDictionary> result) + { + if (!_logger.IsEnabled(LogLevel.Debug)) + return; + + var attributeCount = result.Sum(kvp => kvp.Value.Count); + _logger.LogDebug( + "Generated {AttributeCount} reply attribute values in {GroupCount} groups", + attributeCount, + result.Count); + } + + private static string GetLoggableValue(object value) + { + if (value is IPAddress ip) + return ip.ToString(); + if (value is DateTime dt) + return dt.ToString("O"); + if (value is string str && str.Length > 50) + return $"{str[..50]}..."; + + return value.ToString() ?? "null"; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/IRadiusPacketValidator.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/IRadiusPacketValidator.cs new file mode 100644 index 00000000..467f76c3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/IRadiusPacketValidator.cs @@ -0,0 +1,10 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; + +public interface IRadiusPacketValidator +{ + void ValidateRawPacket(byte[] packetBytes); + void ValidateParsedPacket(RadiusPacket packet, SharedSecret sharedSecret); + void ValidatePacketForSerialization(RadiusPacket packet); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs new file mode 100644 index 00000000..fa9de0f3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs @@ -0,0 +1,129 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; + +public class RadiusPacketValidator : IRadiusPacketValidator +{ + private readonly ILogger _logger; + + public RadiusPacketValidator(ILogger logger) + { + _logger = logger; + } + + public void ValidateRawPacket(byte[] packetBytes) + { + ArgumentNullException.ThrowIfNull(packetBytes); + + if (packetBytes.Length < 20) + throw new InvalidOperationException($"Packet too short: {packetBytes.Length} bytes"); + + if (packetBytes.Length > 4096) + throw new InvalidOperationException($"Packet too large: {packetBytes.Length} bytes"); + + byte code = packetBytes[0]; + if (!Enum.IsDefined(typeof(PacketCode), (int)code)) + throw new InvalidOperationException($"Invalid packet code: {code}"); + + ushort declaredLength = BitConverter.ToUInt16([packetBytes[3], packetBytes[2]], 0); + if (declaredLength != packetBytes.Length) + throw new InvalidOperationException( + $"Packet length mismatch. Declared: {declaredLength}, Actual: {packetBytes.Length}"); + + _logger.LogDebug("Raw packet validation passed: Length={Length}, Code={Code}", + packetBytes.Length, (PacketCode)code); + } + + public void ValidateParsedPacket(RadiusPacket packet, SharedSecret sharedSecret) + { + ArgumentNullException.ThrowIfNull(packet); + ArgumentNullException.ThrowIfNull(sharedSecret); + + if (!Enum.IsDefined(typeof(PacketCode), packet.Code)) + throw new InvalidOperationException($"Invalid packet code: {packet.Code}"); + + if (packet.Authenticator.Value.Length != 16) + throw new InvalidOperationException("Authenticator must be 16 bytes"); + + if (packet.RequestAuthenticator != null && packet.RequestAuthenticator.Value.Length != 16) + throw new InvalidOperationException("Request authenticator must be 16 bytes"); + + switch (packet.Code) + { + case PacketCode.AccessRequest: + ValidateAccessRequest(packet); + break; + case PacketCode.AccountingRequest: + ValidateAccountingRequest(packet); + break; + case PacketCode.DisconnectRequest: + case PacketCode.CoaRequest: + ValidateCoaRequest(packet); + break; + } + + _logger.LogDebug("Parsed packet validation passed: Code={Code}, Id={Id}, Attributes={AttributeCount}", + packet.Code, packet.Identifier, packet.Attributes.Count); + } + + public void ValidatePacketForSerialization(RadiusPacket packet) + { + if (packet == null) + throw new ArgumentNullException(nameof(packet)); + + if (!Enum.IsDefined(typeof(PacketCode), packet.Code)) + throw new InvalidOperationException($"Invalid packet code for serialization: {packet.Code}"); + + if (packet.Authenticator.Value.Length != 16) + throw new InvalidOperationException("Authenticator must be 16 bytes for serialization"); + + switch (packet.Code) + { + case PacketCode.AccessAccept: + case PacketCode.AccessReject: + case PacketCode.AccessChallenge: + if (packet.RequestAuthenticator == null) + { + _logger.LogWarning("Response packet missing request authenticator: Code={Code}", packet.Code); + } + break; + } + + _logger.LogDebug("Packet ready for serialization: Code={Code}, Id={Id}", packet.Code, packet.Identifier); + } + + private void ValidateAccessRequest(RadiusPacket packet) + { + if (!packet.HasAttribute("User-Name")) + { + _logger.LogWarning("Access-Request missing User-Name attribute"); + } + + bool hasPassword = packet.HasAttribute("User-Password"); + bool hasChapPassword = packet.HasAttribute("CHAP-Password"); + bool hasChapChallenge = packet.HasAttribute("CHAP-Challenge"); + + if (!hasPassword && !(hasChapPassword && hasChapChallenge)) + { + _logger.LogWarning("Access-Request missing authentication credentials"); + } + } + + private static void ValidateAccountingRequest(RadiusPacket packet) + { + if (!packet.HasAttribute("Acct-Status-Type")) + { + throw new InvalidOperationException("Accounting-Request missing Acct-Status-Type"); + } + } + + private void ValidateCoaRequest(RadiusPacket packet) + { + if (!packet.HasAttribute("User-Name") && !packet.HasAttribute("Acct-Session-Id")) + { + _logger.LogWarning("CoA/Disconnect request missing User-Name or Acct-Session-Id"); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs new file mode 100644 index 00000000..d122eedc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; + +public static class BytesExtensions +{ + public static string? ToBase64(this byte[]? bytes) + { + return bytes != null ? Convert.ToBase64String(bytes) : null; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs new file mode 100644 index 00000000..419e8e11 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs @@ -0,0 +1,47 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; + +public static class HttpRequestMessageExtension +{ + public static HttpRequestMessage CloneHttpRequestMessage(this HttpRequestMessage original) + { + var clone = new HttpRequestMessage(original.Method, original.RequestUri); + + // Копируем содержимое + if (original.Content != null) + { + // Для StreamContent и ByteArrayContent используем оригинал + if (original.Content is StreamContent || + original.Content is ByteArrayContent || + original.Content is StringContent) + { + clone.Content = original.Content; + } + else + { + // Для других типов читаем и создаем новое содержимое + var content = original.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + clone.Content = new StringContent(content, Encoding.UTF8, + original.Content.Headers.ContentType?.MediaType ?? "application/json"); + } + } + + // Копируем заголовки + foreach (var header in original.Headers) + { + clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + // Копируем свойства запроса + foreach (var prop in original.Options) + { + clone.Options.TryAdd(prop.Key, prop.Value); + } + + // Копируем версию + clone.Version = original.Version; + + return clone; + } +} diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs new file mode 100644 index 00000000..e176c732 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs @@ -0,0 +1,68 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; + +public static class StringExtension +{ + public static string[] CustomSplit(this string? target, string separator = ";") + { + return target? + .Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? []; + } + + public static string CanonicalizeUserName(this string userName) + { + if (string.IsNullOrEmpty(userName)) + { + throw new ArgumentNullException(nameof(userName)); + } + + var identity = userName.ToLower(); + var index = identity.IndexOf('\\', StringComparison.Ordinal); + if (index > 0) + { + identity = identity[(index + 1)..]; + } + + index = identity.IndexOf('@', StringComparison.Ordinal); + if (index > 0) + { + identity = identity[..index]; + } + + return identity; + } + + /// + /// Check if username does not contains domain prefix or suffix + /// + public static bool IsCanonicalUserName(this string userName) + { + if (string.IsNullOrEmpty(userName)) + { + throw new ArgumentNullException(nameof(userName)); + } + + return userName.IndexOfAny(new[] { '\\', '@' }) == -1; + } + + public static byte[] ToByteArray(this string hex) + { + ArgumentException.ThrowIfNullOrWhiteSpace(hex); + + var bytes = new byte[hex.Length / 2]; + for (int i = 0; i < hex.Length; i += 2) + { + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + } + + return bytes; + } + + public static string FromBase64ToUtf8(this string st) + { + return Encoding.UTF8.GetString(Convert.FromBase64String(st)); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Multifactor.Radius.Adapter.v2.Shared.csproj b/src/Multifactor.Radius.Adapter.v2.Shared/Multifactor.Radius.Adapter.v2.Shared.csproj new file mode 100644 index 00000000..3a635329 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Multifactor.Radius.Adapter.v2.Shared.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs deleted file mode 100644 index 24325785..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -namespace Multifactor.Radius.Adapter.v2.Tests.AccessChallengeTests; - -public class ChallengeProcessorProviderTests -{ - [Theory] - [InlineData(ChallengeType.PasswordChange)] - [InlineData(ChallengeType.SecondFactor)] - public void GetProcessor_ByType_ShouldReturnProcessor(ChallengeType challengeType) - { - var processor = new Mock(); - processor.Setup(x => x.ChallengeType).Returns(challengeType); - var provider = new ChallengeProcessorProvider([processor.Object]); - var actual = provider.GetChallengeProcessorByType(challengeType); - - Assert.NotNull(actual); - Assert.Equal(challengeType, actual.ChallengeType); - } - - [Fact] - public void GetProcessor_ByChallengeIdentifier_ShouldReturnProcessor() - { - var processorMock = new Mock(); - var identifier = new ChallengeIdentifier("user", "id"); - processorMock.Setup(x => x.HasChallengeContext(It.IsAny())).Returns(true); - var processor = processorMock.Object; - var provider = new ChallengeProcessorProvider([processor]); - var actual = provider.GetChallengeProcessorByIdentifier(identifier); - - Assert.NotNull(actual); - Assert.Equal(processor, actual); - } - - [Theory] - [InlineData(ChallengeType.PasswordChange)] - [InlineData(ChallengeType.SecondFactor)] - public void GetProcessor_NoSuchType_ShouldReturnNull(ChallengeType challengeType) - { - var processor = new Mock(); - processor.Setup(x => x.ChallengeType).Returns(ChallengeType.None); - var provider = new ChallengeProcessorProvider([processor.Object]); - var actual = provider.GetChallengeProcessorByType(challengeType); - - Assert.Null(actual); - } - - [Fact] - public void GetProcessor_NoSuchChallengeIdentifier_ShouldReturnNull() - { - var processorMock = new Mock(); - var identifier = new ChallengeIdentifier("user", "id"); - processorMock.Setup(x => x.HasChallengeContext(It.IsAny())).Returns(false); - var processor = processorMock.Object; - var provider = new ChallengeProcessorProvider([processor]); - var actual = provider.GetChallengeProcessorByIdentifier(identifier); - - Assert.Null(actual); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs deleted file mode 100644 index c0130920..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs +++ /dev/null @@ -1,370 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.DataProtection; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.AccessChallengeTests; - -public class ChangePasswordChallengeProcessorTests -{ - [Fact] - public void ShouldReturnCorrectChallengeType() - { - //Arrange - var memCacheMock = new Mock(); - var service = new Mock(); - var dataProtectionService = new Mock(); - - //Act - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService.Object, NullLogger.Instance); - - //Assert - Assert.Equal(ChallengeType.PasswordChange, processor.ChallengeType); - } - - [Fact] - public void AddChallengeContext_NoContext_ShouldThrowArgumentNullException() - { - //Arrange - var memCacheMock = new Mock(); - var service = new Mock(); - var dataProtectionService = new Mock(); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService.Object, NullLogger.Instance); - - //Act - //Assert - Assert.Throws(() => processor.AddChallengeContext(null)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void AddChallengeContext_NoPassword_ShouldThrowArgumentNullException(string emptyString) - { - //Arrange - var memCacheMock = new Mock(); - var service = new Mock(); - var dataProtectionService = new Mock(); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse(emptyString, PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - var context = contextMock.Object; - - //Act - //Assert - Assert.Throws(() => processor.AddChallengeContext(context)); - } - - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void AddChallengeContext_NoDomain_ShouldThrowArgumentNullException(string emptyString) - { - //Arrange - var memCacheMock = new Mock(); - var service = new Mock(); - var dataProtectionService = new Mock(); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns(emptyString); - var context = contextMock.Object; - - //Act - //Assert - Assert.Throws(() => processor.AddChallengeContext(context)); - } - - [Fact] - public void AddChallengeContext_ShouldAdd() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.SetupProperty(x => x.ResponseInformation); - var context = contextMock.Object; - context.ResponseInformation = new ResponseInformation(); - - //Act - var id = processor.AddChallengeContext(context); - - //Assert - Assert.NotNull(id); - Assert.NotNull(context.ResponseInformation.State); - Assert.NotNull(context.ResponseInformation.ReplyMessage); - Assert.NotEmpty(context.ResponseInformation.State); - Assert.NotEmpty(context.ResponseInformation.ReplyMessage); - memCacheMock.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task ProcessChallenge_EmptyContext_ShouldThrowArgumentNullException() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var id = new ChallengeIdentifier("1", "2"); - - //Act - //Assert - await Assert.ThrowsAsync(() => processor.ProcessChallengeAsync(id, null)); - } - - [Fact] - public async Task ProcessChallenge_NoRequest_ShouldReturnAccept() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - PasswordChangeRequest obj = null; - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out obj)).Returns(false); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.ResponseInformation); - var context = contextMock.Object; - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.Accept, status); - } - - [Fact] - public async Task ProcessChallenge_NoPassword_ShouldReturnReject() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - var passwordRequest = new PasswordChangeRequest(); - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out passwordRequest)).Returns(true); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse(null, PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.SetupProperty(x => x.AuthenticationState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.Reject, status); - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessChallenge_NoNewPassword_ShouldReturnInProcess() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - var passwordRequest = new PasswordChangeRequest(); - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out passwordRequest)).Returns(true); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.SetupProperty(x => x.AuthenticationState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.InProcess, status); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessChallenge_NotMatchChallenge_ShouldReturnInProcess() - { - //Arrange - var service = new Mock(); - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - var dataProtectionService = dataProtectionServiceMock.Object; - var memCacheMock = new Mock(); - var passwordRequest = new PasswordChangeRequest() { NewPasswordEncryptedData = "password" }; - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out passwordRequest)).Returns(true); - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.SetupProperty(x => x.AuthenticationState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.InProcess, status); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - Assert.StartsWith("Passwords not match", context.ResponseInformation.ReplyMessage); - } - - [Fact] - public async Task ProcessChallenge_SuccessfulPasswordChange_ShouldReturnAccept() - { - //Arrange - var service = new Mock(); - service - .Setup(x => x.ChangeUserPasswordAsync(It.IsAny())) - .ReturnsAsync(() => new PasswordChangeResponse() { Success = true }); - - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - dataProtectionServiceMock.Setup(x => x.Unprotect(It.IsAny(), It.IsAny())).Returns("1234567"); - var dataProtectionService = dataProtectionServiceMock.Object; - - var memCacheMock = new Mock(); - var passwordRequest = new PasswordChangeRequest() { NewPasswordEncryptedData = "1234567" }; - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out passwordRequest)).Returns(true); - - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("1234567", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.SetupProperty(x => x.AuthenticationState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.Accept, status); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - Assert.Null(context.ResponseInformation.State); - } - - [Fact] - public async Task ProcessChallenge_UnsuccessfulPasswordChange_ShouldReturnReject() - { - //Arrange - var service = new Mock(); - service - .Setup(x => x.ChangeUserPasswordAsync(It.IsAny())) - .ReturnsAsync(() => new PasswordChangeResponse() { Success = false }); - - var dataProtectionServiceMock = new Mock(); - dataProtectionServiceMock.Setup(x => x.Protect(It.IsAny(), It.IsAny())).Returns("password"); - dataProtectionServiceMock.Setup(x => x.Unprotect(It.IsAny(), It.IsAny())).Returns("1234567"); - var dataProtectionService = dataProtectionServiceMock.Object; - - var memCacheMock = new Mock(); - var passwordRequest = new PasswordChangeRequest() { NewPasswordEncryptedData = "1234567" }; - memCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out passwordRequest)).Returns(true); - - var processor = new ChangePasswordChallengeProcessor(memCacheMock.Object, service.Object, dataProtectionService, NullLogger.Instance); - var contextMock = new Mock(); - var passphrase = UserPassphrase.Parse("1234567", PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(passphrase); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.MustChangePasswordDomain).Returns("domain"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.SetupProperty(x => x.AuthenticationState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - var request = new ChallengeIdentifier("1", "2"); - - //Act - var status = await processor.ProcessChallengeAsync(request, context); - - //Assert - Assert.Equal(ChallengeStatus.Reject, status); - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.FirstFactorStatus); - Assert.Null(context.ResponseInformation.State); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs deleted file mode 100644 index 4c25ae0f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.AccessChallengeTests; - -public class SecondFactorChallengeProcessorTests -{ - [Fact] - public void ShouldReturnCorrectChallengeType() - { - var mfServiceMock = new Mock(); - var groupsServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var processor = - new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - Assert.Equal(ChallengeType.SecondFactor, processor.ChallengeType); - } - - [Fact] - public void AddChallengeContext_NoContext_ShouldThrowArgumentNullException() - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = - new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - - Assert.Throws(() => processor.AddChallengeContext(null)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void AddChallengeContext_NoState_ShouldThrowArgumentException(string emptyString) - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = - new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.ResponseInformation.State).Returns(emptyString); - Assert.ThrowsAny(() => processor.AddChallengeContext(contextMock.Object)); - } - - [Fact] - public void AddChallengeContext_ShouldAdd() - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = - new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - - var id = processor.AddChallengeContext(contextMock.Object); - Assert.NotNull(id); - Assert.Equal("state", id.RequestId); - Assert.True(processor.HasChallengeContext(id)); - } - - [Fact] - public void AddChallengeContext_SameId_ShouldNotAdd() - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = - new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - var context = contextMock.Object; - - processor.AddChallengeContext(context); - var id = processor.AddChallengeContext(context); - Assert.NotNull(id); - Assert.Empty(id.RequestId); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task ProcessChallenge_EmptyName_ShouldReject(string userName) - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns(userName); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - - var context = contextMock.Object; - var id = new ChallengeIdentifier("1", "2"); - var status = await processor.ProcessChallengeAsync(id, context); - - Assert.Equal(ChallengeStatus.Reject, status); - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.SecondFactorStatus); - Assert.Equal(id.RequestId, context.ResponseInformation.State); - } - - [Theory] - [InlineData(AuthenticationType.Unknown)] - [InlineData(AuthenticationType.EAP)] - [InlineData(AuthenticationType.CHAP)] - [InlineData(AuthenticationType.MSCHAP)] - public async Task ProcessAuthenticationType_UnsupportedType_ShouldReject(AuthenticationType authType) - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.AuthenticationType).Returns(authType); - contextMock.Setup(x => x.RequestPacket.TryGetUserPassword()).Returns("password"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - - var context = contextMock.Object; - var id = new ChallengeIdentifier("1", "2"); - var status = await processor.ProcessChallengeAsync(id, context); - - Assert.Equal(ChallengeStatus.Reject, status); - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.SecondFactorStatus); - Assert.Equal(id.RequestId, context.ResponseInformation.State); - } - - [Theory] - [InlineData(AuthenticationType.PAP)] - [InlineData(AuthenticationType.MSCHAP2)] - public async Task ProcessAuthenticationType_SupportedTypeNoContext_ShouldReject(AuthenticationType authType) - { - var mfServiceMock = new Mock(); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.AuthenticationType).Returns(authType); - contextMock.Setup(x => x.RequestPacket.TryGetUserPassword()).Returns("password"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - - var context = contextMock.Object; - var id = new ChallengeIdentifier("1", "2"); - await Assert.ThrowsAsync(() => processor.ProcessChallengeAsync(id, context)); - } - - [Fact] - public async Task ProcessAuthenticationType_PositiveApiResponse_ShouldAccept() - { - var mfServiceMock = new Mock(); - mfServiceMock - .Setup(x => x.SendChallengeAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Accept)); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.AuthenticationType).Returns(AuthenticationType.PAP); - contextMock.Setup(x => x.RequestPacket.TryGetUserPassword()).Returns("password"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("1"); - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1", "2")); - var ldapConfigMock = new Mock(); - ldapConfigMock.Setup(x => x.AuthenticationCacheGroups).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - var context = contextMock.Object; - - context.ResponseInformation.State = "2"; - var id = new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); - processor.AddChallengeContext(context); - var result = await processor.ProcessChallengeAsync(id, context); - Assert.Equal(ChallengeStatus.Accept, result); - Assert.Equal(AuthenticationStatus.Accept, context.AuthenticationState.SecondFactorStatus); - Assert.False(processor.HasChallengeContext(id)); - } - - [Fact] - public async Task ProcessAuthenticationType_NegativeApiResponse_ShouldAccept() - { - var mfServiceMock = new Mock(); - mfServiceMock - .Setup(x => x.SendChallengeAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Reject)); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.AuthenticationType).Returns(AuthenticationType.PAP); - contextMock.Setup(x => x.RequestPacket.TryGetUserPassword()).Returns("password"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.ClientConfigurationName).Returns("1"); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1", "2")); - var ldapConfigMock = new Mock(); - ldapConfigMock.Setup(x => x.AuthenticationCacheGroups).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - var context = contextMock.Object; - - context.ResponseInformation.State = "2"; - var id = new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); - processor.AddChallengeContext(context); - var result = await processor.ProcessChallengeAsync(id, context); - Assert.Equal(ChallengeStatus.Reject, result); - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.SecondFactorStatus); - Assert.False(processor.HasChallengeContext(id)); - } - - [Theory] - [InlineData(AuthenticationStatus.Awaiting)] - [InlineData(AuthenticationStatus.Bypass)] - public async Task ProcessAuthenticationType_NeutralApiResponse_ShouldAccept(AuthenticationStatus status) - { - var mfServiceMock = new Mock(); - mfServiceMock - .Setup(x => x.SendChallengeAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(status)); - var mfService = mfServiceMock.Object; - var groupsServiceMock = new Mock(); - var processor = new SecondFactorChallengeProcessor(mfService, groupsServiceMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var endpoint = IPEndPoint.Parse("127.0.0.1:8080"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.AuthenticationType).Returns(AuthenticationType.PAP); - contextMock.Setup(x => x.RequestPacket.TryGetUserPassword()).Returns("password"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(endpoint); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("1"); - contextMock.SetupProperty(x => x.AuthenticationState.SecondFactorStatus); - contextMock.SetupProperty(x => x.ResponseInformation.State); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1", "2")); - var ldapConfigMock = new Mock(); - ldapConfigMock.Setup(x => x.AuthenticationCacheGroups).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - var context = contextMock.Object; - - context.ResponseInformation.State = "2"; - var id = new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); - processor.AddChallengeContext(context); - var result = await processor.ProcessChallengeAsync(id, context); - Assert.Equal(ChallengeStatus.InProcess, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Multifactor/MultifactorApiServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Multifactor/MultifactorApiServiceTests.cs new file mode 100644 index 00000000..20b3f87b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Multifactor/MultifactorApiServiceTests.cs @@ -0,0 +1,627 @@ +// using System.Net; +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Core.Ldap.Entry; +// using Multifactor.Radius.Adapter.v2.Application.Cache; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Application.Multifactor +// { +// public class MultifactorApiServiceTests +// { +// private readonly Mock _apiMock; +// private readonly Mock _cacheMock; +// private readonly Mock> _loggerMock; +// private readonly MultifactorApiService _service; +// +// public MultifactorApiServiceTests() +// { +// _apiMock = new Mock(); +// _cacheMock = new Mock(); +// _loggerMock = new Mock>(); +// _service = new MultifactorApiService(_apiMock.Object, _cacheMock.Object, _loggerMock.Object); +// } +// +// [Fact] +// public void Constructor_ShouldThrowArgumentNullException_WhenApiIsNull() +// { +// // Act & Assert +// Assert.Throws(() => +// new MultifactorApiService(null, _cacheMock.Object, _loggerMock.Object)); +// } +// +// [Fact] +// public void Constructor_ShouldThrowArgumentNullException_WhenCacheIsNull() +// { +// // Act & Assert +// Assert.Throws(() => +// new MultifactorApiService(_apiMock.Object, null, _loggerMock.Object)); +// } +// +// [Fact] +// public void Constructor_ShouldThrowArgumentNullException_WhenLoggerIsNull() +// { +// // Act & Assert +// Assert.Throws(() => +// new MultifactorApiService(_apiMock.Object, _cacheMock.Object, null)); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Act & Assert +// await Assert.ThrowsAsync(() => _service.CreateSecondFactorRequestAsync(null, false)); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldReturnReject_WhenIdentityIsEmpty() +// { +// // Arrange +// +// var header = new RadiusPacketHeader(); +// var requestPacket = new RadiusPacket(header) +// { +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812) +// }; +// var clientConfiguration = new ClientConfiguration(); +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration) ; +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, false); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Reject, result.Code); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldReturnBypass_WhenCacheHit() +// { +// // Arrange +// var requestPacket = new RadiusPacket(It.IsAny()) +// { +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812), +// }; +// requestPacket.AddAttributeValue("Calling-Station-Id", "192.168.1.1"); +// requestPacket.AddAttributeValue("User-Name", "testuser"); +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// AuthenticationCacheLifetime = TimeSpan.FromMinutes(30) +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration); +// +// _cacheMock.Setup(x => x.TryHitCache( +// It.IsAny(), +// "testuser", +// "TestClient", +// TimeSpan.FromMinutes(30))) +// .Returns(true); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, false); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Bypass, result.Code); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldCallApi_WhenCacheMiss() +// { +// // Arrange +// var requestPacket = new RadiusPacket(It.IsAny()) +// { +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812) +// }; +// requestPacket.AddAttributeValue("Calling-Station-Id", "192.168.1.1"); +// requestPacket.AddAttributeValue("User-Name", "testuser"); +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorNasIdentifier = "nas-id", +// MultifactorSharedSecret = "shared-secret" +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration) +// { +// LdapProfile = new LdapProfile(It.IsAny()) +// { +// DisplayName = "Test User", +// Email = "test@example.com", +// Phone = "+1234567890" +// } +// }; +// +// var expectedResponse = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// ReplyMessage = "Granted", +// Id = "request-id-123" +// }; +// +// _cacheMock.Setup(x => x.TryHitCache( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Returns(false); +// +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ReturnsAsync(expectedResponse); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, true); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Accept, result.Code); +// Assert.Equal("request-id-123", result.State); +// Assert.Equal("Granted", result.ReplyMessage); +// _apiMock.Verify(x => x.CreateAccessRequest( +// It.Is(q => q.Identity == "testuser"), +// It.Is(a => a.ApiSecret == "nas-id"), +// It.IsAny()), +// Times.Once); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldCacheResponse_WhenEnabledAndAccepted() +// { +// // Arrange +// var header = new RadiusPacketHeader(); +// +// var requestPacket = new RadiusPacket(header) +// { +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812), +// }; +// requestPacket.AddAttributeValue("Calling-Station-Id", "192.168.1.1"); +// requestPacket.AddAttributeValue("User-Name", "testuser"); +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorNasIdentifier = "nas-id", +// MultifactorSharedSecret = "shared-secret", +// AuthenticationCacheLifetime = TimeSpan.FromMinutes(30) +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration); +// +// var apiResponse = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// Bypassed = false +// }; +// +// _cacheMock.Setup(x => x.TryHitCache( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Returns(false); +// +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, true); +// +// // Assert +// _cacheMock.Verify(x => x.SetCache( +// It.IsAny(), +// "testuser", +// "TestClient", +// TimeSpan.FromMinutes(30)), +// Times.Once); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldNotCache_WhenBypassed() +// { +// // Arrange +// var header = new RadiusPacketHeader(); +// var requestPacket = new RadiusPacket(header) +// { +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812) +// }; +// requestPacket.AddAttributeValue("User-Name", "testuser"); +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient" +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration); +// +// var apiResponse = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// Bypassed = true +// }; +// +// _cacheMock.Setup(x => x.TryHitCache( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Returns(false); +// +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, true); +// +// // Assert +// _cacheMock.Verify(x => x.SetCache( +// It.IsAny(), +// It.IsAny(), +// It.IsAny(), +// It.IsAny()), +// Times.Never); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldApplyFullPrivacyMode() +// { +// // Arrange +// var header = new RadiusPacketHeader(); +// var requestPacket = new RadiusPacket(header) +// { +// UserName = "testuser", +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812), +// CallingStationIdAttribute = "192.168.1.1" +// }; +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorNasIdentifier = "nas-id", +// MultifactorSharedSecret = "shared-secret", +// Privacy = (PrivacyMode: PrivacyMode.Full, PrivacyFields: []) +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration) +// { +// LdapProfile = new LdapProfile() +// { +// DisplayName = "Test User", +// Email = "test@example.com", +// Phone = "+1234567890" +// } +// }; +// +// AccessRequestQuery capturedQuery = null; +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Callback((q, a) => capturedQuery = q) +// .ReturnsAsync(new AccessRequestResponse { Status = RequestStatus.Granted }); +// +// // Act +// await _service.CreateSecondFactorRequestAsync(context, false); +// +// // Assert +// Assert.NotNull(capturedQuery); +// Assert.Null(capturedQuery.Name); +// Assert.Null(capturedQuery.Email); +// Assert.Null(capturedQuery.Phone); +// Assert.Equal("", capturedQuery.CallingStationId); +// Assert.Null(capturedQuery.CalledStationId); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldReturnBypass_WhenApiUnreachableAndBypassEnabled() +// { +// // Arrange +// var header = new RadiusPacketHeader(); +// var requestPacket = new RadiusPacket(header) +// { +// UserName = "testuser", +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812) +// }; +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// BypassSecondFactorWhenApiUnreachable = true +// }; +// var ldapConfiguration = new LdapServerConfiguration() +// { +// BypassSecondFactorWhenApiUnreachableGroups = new List { "group1" } +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration, ldapConfiguration) +// { +// +// UserGroups = ["group1"] +// }; +// +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ThrowsAsync(new MultifactorApiUnreachableException("API unreachable")); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, false); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Bypass, result.Code); +// } +// +// [Fact] +// public async Task CreateSecondFactorRequestAsync_ShouldReturnReject_WhenApiUnreachableAndBypassDisabled() +// { +// // Arrange +// var header = new RadiusPacketHeader(); +// var requestPacket = new RadiusPacket(header) +// { +// UserName = "testuser", +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812) +// }; +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// BypassSecondFactorWhenApiUnreachable = false +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration); +// +// _apiMock.Setup(x => x.CreateAccessRequest( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ThrowsAsync(new MultifactorApiUnreachableException("API unreachable")); +// +// // Act +// var result = await _service.CreateSecondFactorRequestAsync(context, false); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Reject, result.Code); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Act & Assert +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(null, false, "request-id", "answer")); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldThrowArgumentException_WhenRequestIdIsNullOrEmpty() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()); +// +// // Act & Assert +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(context, false, "", "answer")); +// +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(context, false, null, "answer")); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldThrowArgumentException_WhenAnswerIsNullOrEmpty() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()); +// +// // Act & Assert +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(context, false, "request-id", "")); +// +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(context, false, "request-id", null)); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldThrowInvalidOperationException_WhenIdentityIsEmpty() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "" +// } +// }; +// +// // Act & Assert +// await Assert.ThrowsAsync(() => +// _service.SendChallengeAsync(context, false, "request-id", "answer")); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldCallApiWithCorrectParameters() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812), +// CallingStationIdAttribute = "192.168.1.1" +// }, +// ClientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorNasIdentifier = "nas-id", +// MultifactorSharedSecret = "shared-secret", +// AuthenticationCacheLifetime = TimeSpan.FromMinutes(30) +// } +// }; +// +// var expectedResponse = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// ReplyMessage = "Accepted" +// }; +// +// _apiMock.Setup(x => x.SendChallengeAsync( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ReturnsAsync(expectedResponse); +// +// // Act +// var result = await _service.SendChallengeAsync(context, true, "request-123", "123456"); +// +// // Assert +// _apiMock.Verify(x => x.SendChallengeAsync( +// It.Is(q => +// q.Identity == "testuser" && +// q.Challenge == "123456" && +// q.RequestId == "request-123"), +// It.Is(a => +// a.ApiKey == "nas-id" && +// a.ApiSecret == "shared-secret")), +// Times.Once); +// } +// +// [Fact] +// public async Task SendChallengeAsync_ShouldCacheResponse_WhenEnabledAndAccepted() +// { +// // Arrange +// var requestPacket = new RadiusPacket +// { +// UserName = "testuser", +// RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1812), +// CallingStationIdAttribute = "192.168.1.1" +// }; +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorNasIdentifier = "nas-id", +// MultifactorSharedSecret = "shared-secret", +// AuthenticationCacheLifetime = TimeSpan.FromMinutes(30) +// }; +// var context = new RadiusPipelineContext(requestPacket, clientConfiguration); +// +// var apiResponse = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// Bypassed = false +// }; +// +// _apiMock.Setup(x => x.SendChallengeAsync( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _service.SendChallengeAsync(context, true, "request-id", "answer"); +// +// // Assert +// _cacheMock.Verify(x => x.SetCache( +// It.IsAny(), +// "testuser", +// "TestClient", +// TimeSpan.FromMinutes(30)), +// Times.Once); +// } +// +// [Fact] +// public void ConvertToAuthCode_ShouldReturnBypass_WhenGrantedAndBypassed() +// { +// // Arrange +// var response = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// Bypassed = true +// }; +// +// // Act +// var result = InvokePrivateMethod("ConvertToAuthCode", response); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Bypass, result); +// } +// +// [Fact] +// public void ConvertToAuthCode_ShouldReturnAccept_WhenGrantedAndNotBypassed() +// { +// // Arrange +// var response = new AccessRequestResponse +// { +// Status = RequestStatus.Granted, +// Bypassed = false +// }; +// +// // Act +// var result = InvokePrivateMethod("ConvertToAuthCode", response); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Accept, result); +// } +// +// [Fact] +// public void ConvertToAuthCode_ShouldReturnReject_WhenDenied() +// { +// // Arrange +// var response = new AccessRequestResponse +// { +// Status = RequestStatus.Denied +// }; +// +// // Act +// var result = InvokePrivateMethod("ConvertToAuthCode", response); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Reject, result); +// } +// +// [Fact] +// public void ConvertToAuthCode_ShouldReturnAwaiting_WhenAwaitingAuthentication() +// { +// // Arrange +// var response = new AccessRequestResponse +// { +// Status = RequestStatus.AwaitingAuthentication +// }; +// +// // Act +// var result = InvokePrivateMethod("ConvertToAuthCode", response); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Awaiting, result); +// } +// +// [Fact] +// public void ConvertToAuthCode_ShouldReturnReject_WhenResponseIsNull() +// { +// // Act +// var result = InvokePrivateMethod("ConvertToAuthCode", (AccessRequestResponse)null); +// +// // Assert +// Assert.Equal(AuthenticationStatus.Reject, result); +// } +// +// private static T InvokePrivateMethod(string methodName, params object[] parameters) +// { +// var method = typeof(MultifactorApiService).GetMethod( +// methodName, +// System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); +// +// if (method == null) +// throw new ArgumentException($"Method {methodName} not found"); +// +// return (T)method.Invoke(new MultifactorApiService( +// Mock.Of(), +// Mock.Of(), +// Mock.Of>()), parameters); +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChallengeProcessorProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChallengeProcessorProviderTests.cs new file mode 100644 index 00000000..84ced1cb --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChallengeProcessorProviderTests.cs @@ -0,0 +1,103 @@ +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.AccessChallenge +{ + public class ChallengeProcessorProviderTests + { + [Fact] + public void GetChallengeProcessorByIdentifier_ShouldReturnProcessorWithContext() + { + // Arrange + var identifier = new ChallengeIdentifier("client1", "request123"); + var processor1 = new Mock(); + processor1.Setup(x => x.HasChallengeContext(identifier)).Returns(false); + var processor2 = new Mock(); + processor2.Setup(x => x.HasChallengeContext(identifier)).Returns(true); + var provider = new ChallengeProcessorProvider(new[] { processor1.Object, processor2.Object }); + + // Act + var result = provider.GetChallengeProcessorByIdentifier(identifier); + + // Assert + Assert.Equal(processor2.Object, result); + } + + [Fact] + public void GetChallengeProcessorByIdentifier_ShouldReturnNullWhenNoProcessorHasContext() + { + // Arrange + var identifier = new ChallengeIdentifier("client1", "request123"); + + var processor = new Mock(); + processor.Setup(x => x.HasChallengeContext(identifier)).Returns(false); + var provider = new ChallengeProcessorProvider([processor.Object]); + + // Act + var result = provider.GetChallengeProcessorByIdentifier(identifier); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetChallengeProcessorByIdentifier_ShouldThrowWhenIdentifierNull() + { + // Arrange + var provider = new ChallengeProcessorProvider([]); + + // Act & Assert + Assert.Throws(() => provider.GetChallengeProcessorByIdentifier(null)); + } + + [Fact] + public void GetChallengeProcessorByType_ShouldReturnCorrectProcessor() + { + // Arrange + var processor1 = new Mock(); + processor1.Setup(x => x.ChallengeType).Returns(ChallengeType.SecondFactor); + + var processor2 = new Mock(); + processor2.Setup(x => x.ChallengeType).Returns(ChallengeType.PasswordChange); + + var provider = new ChallengeProcessorProvider([processor1.Object, processor2.Object]); + + // Act + var result = provider.GetChallengeProcessorByType(ChallengeType.PasswordChange); + + // Assert + Assert.Equal(processor2.Object, result); + } + + [Fact] + public void GetChallengeProcessorByType_ShouldReturnNullWhenTypeNotFound() + { + // Arrange + var processor = new Mock(); + processor.Setup(x => x.ChallengeType).Returns(ChallengeType.SecondFactor); + + var provider = new ChallengeProcessorProvider(new[] { processor.Object }); + + // Act + var result = provider.GetChallengeProcessorByType(ChallengeType.PasswordChange); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetChallengeProcessorByType_ShouldReturnNullWhenNoProcessors() + { + // Arrange + var provider = new ChallengeProcessorProvider(Array.Empty()); + + // Act + var result = provider.GetChallengeProcessorByType(ChallengeType.SecondFactor); + + // Assert + Assert.Null(result); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChangePasswordChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChangePasswordChallengeProcessorTests.cs new file mode 100644 index 00000000..80888555 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChangePasswordChallengeProcessorTests.cs @@ -0,0 +1,352 @@ +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Core.Ldap.Schema; +// using Multifactor.Radius.Adapter.v2.Application.Cache; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Security; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.AccessChallenge +// { +// public class ChangePasswordChallengeProcessorTests +// { +// private readonly Mock _cacheMock; +// private readonly Mock _ldapAdapterMock; +// private readonly Mock> _loggerMock; +// private readonly ChangePasswordChallengeProcessor _processor; +// +// public ChangePasswordChallengeProcessorTests() +// { +// _cacheMock = new Mock(); +// _ldapAdapterMock = new Mock(); +// _loggerMock = new Mock>(); +// _processor = new ChangePasswordChallengeProcessor(_cacheMock.Object, _ldapAdapterMock.Object, _loggerMock.Object); +// } +// +// [Fact] +// public void ChallengeType_ShouldReturnPasswordChange() +// { +// // Assert +// Assert.Equal(ChallengeType.PasswordChange, _processor.ChallengeType); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Act & Assert +// Assert.Throws(() => _processor.AddChallengeContext(null)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldThrowInvalidOperationException_WhenPasswordIsEmpty() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()) +// { +// Passphrase = It.IsAny() +// }; +// +// // Act & Assert +// Assert.Throws(() => _processor.AddChallengeContext(context)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldThrowInvalidOperationException_WhenDomainIsEmpty() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()) +// { +// Passphrase = UserPassphrase.Parse("oldPassword", PreAuthMode.None), +// MustChangePasswordDomain = "" +// }; +// +// // Act & Assert +// Assert.Throws(() => _processor.AddChallengeContext(context)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldAddChallengeToCacheAndSetResponse() +// { +// // Arrange +// var clientConfiguration = new ClientConfiguration +// { +// Name = "TestClient", +// MultifactorSharedSecret = "sharedSecret" +// }; +// var context = new RadiusPipelineContext(It.IsAny(), clientConfiguration) +// { +// Passphrase = UserPassphrase.Parse("oldPassword", PreAuthMode.None), +// MustChangePasswordDomain = "test.local", +// ResponseInformation = new ResponseInformation() +// }; +// +// PasswordChangeCache capturedCache = null; +// _cacheMock.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) +// .Callback((key, value, expiry) => capturedCache = value as PasswordChangeCache); +// +// // Act +// var identifier = _processor.AddChallengeContext(context); +// +// // Assert +// Assert.NotNull(capturedCache); +// Assert.Equal("test.local", capturedCache.Domain); +// Assert.NotNull(capturedCache.CurrentPasswordEncryptedData); +// Assert.NotNull(capturedCache.Id); +// Assert.Equal(capturedCache.Id, context.ResponseInformation.State); +// Assert.Equal("Please change password to continue. Enter new password: ", context.ResponseInformation.ReplyMessage); +// Assert.Equal("TestClient", identifier.ToString()); +// Assert.Equal(capturedCache.Id, identifier.RequestId); +// _cacheMock.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); +// } +// +// [Fact] +// public void HasChallengeContext_ShouldReturnTrue_WhenCacheHasValue() +// { +// // Arrange +// var requestId = "test-id"; +// _cacheMock.Setup(x => x.TryGetValue(requestId, out It.Ref.IsAny)) +// .Returns(true); +// +// var identifier = new ChallengeIdentifier("client", requestId); +// +// // Act +// var result = _processor.HasChallengeContext(identifier); +// +// // Assert +// Assert.True(result); +// } +// +// [Fact] +// public void HasChallengeContext_ShouldReturnFalse_WhenCacheHasNoValue() +// { +// // Arrange +// var requestId = "test-id"; +// object cacheValue = null; +// _cacheMock.Setup(x => x.TryGetValue(requestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = null)) +// .Returns(false); +// +// var identifier = new ChallengeIdentifier("client", requestId); +// +// // Act +// var result = _processor.HasChallengeContext(identifier); +// +// // Assert +// Assert.False(result); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// +// // Act & Assert +// await Assert.ThrowsAsync(() => _processor.ProcessChallengeAsync(identifier, null)); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnAccept_WhenCacheHasNoRequest() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = It.IsAny(); +// +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Returns(false); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Accept, result); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenRawPasswordIsEmpty() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()) +// { +// Passphrase = It.IsAny(), +// LdapProfile = It.IsAny() +// }; +// +// var passwordChangeRequest = new PasswordChangeCache(); +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out PasswordChangeCache value) => value = passwordChangeRequest)) +// .Returns(true); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); +// Assert.Equal("Password is empty", context.ResponseInformation.ReplyMessage); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnInProcess_WhenNewPasswordNotSet() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()) +// { +// Passphrase = new UserPassphrase { Raw = "newPass1" }, +// ClientConfiguration = new ClientConfiguration { MultifactorSharedSecret = "secret" }, +// LdapProfile = It.IsAny(), +// LdapSchema = It.IsAny(), +// ResponseInformation = new ResponseInformation() +// }; +// +// var passwordChangeRequest = new PasswordChangeCache { Id = "cache-id" }; +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out PasswordChangeCache value) => value = passwordChangeRequest)) +// .Returns(true); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.InProcess, result); +// Assert.Equal("cache-id", context.ResponseInformation.State); +// Assert.Equal("Please repeat new password: ", context.ResponseInformation.ReplyMessage); +// _cacheMock.Verify(x => x.Set(identifier.RequestId, It.IsAny(), It.IsAny()), Times.Once); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnInProcess_WhenPasswordsNotMatch() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = new RadiusPipelineContext +// { +// Passphrase = new UserPassphrase { Raw = "newPass2" }, +// ClientConfiguration = new ClientConfiguration { MultifactorSharedSecret = "secret" }, +// LdapProfile = new LdapProfile(), +// LdapConfiguration = new LdapServerConfiguration(), +// LdapSchema = It.IsAny(), +// ResponseInformation = new ResponseInformation() +// }; +// +// var encryptedPassword = ProtectionService.Protect("secret", "newPass1"); +// var passwordChangeRequest = new PasswordChangeCache +// { +// Id = "cache-id", +// NewPasswordEncryptedData = encryptedPassword +// }; +// +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out PasswordChangeCache value) => value = passwordChangeRequest)) +// .Returns(true); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.InProcess, result); +// Assert.Equal("cache-id", context.ResponseInformation.State); +// Assert.Equal("Passwords not match. Please enter new password: ", context.ResponseInformation.ReplyMessage); +// _cacheMock.Verify(x => x.Set(identifier.RequestId, It.IsAny(), It.IsAny()), Times.Once); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnAccept_WhenPasswordChangeSucceeds() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = new RadiusPipelineContext +// { +// Passphrase = new UserPassphrase { Raw = "newPass1" }, +// ClientConfiguration = new ClientConfiguration { MultifactorSharedSecret = "secret" }, +// LdapProfile = new LdapProfile { Dn = "cn=user,dc=test" }, +// LdapConfiguration = new LdapServerConfiguration() +// { +// ConnectionString = "ldap://test", +// Username = "admin", +// Password = "adminPass", +// BindTimeoutSeconds = 30 +// }, +// LdapSchema = It.IsAny(), +// ResponseInformation = new ResponseInformation() +// }; +// +// var encryptedPassword = ProtectionService.Protect("secret", "newPass1"); +// var passwordChangeRequest = new PasswordChangeCache +// { +// Id = "cache-id", +// NewPasswordEncryptedData = encryptedPassword +// }; +// +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out PasswordChangeCache value) => value = passwordChangeRequest)) +// .Returns(true); +// +// _ldapAdapterMock.Setup(x => x.ChangeUserPassword(It.IsAny())) +// .Returns(true); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Accept, result); +// Assert.Null(context.ResponseInformation.State); +// _cacheMock.Verify(x => x.Remove("cache-id"), Times.Once); +// _ldapAdapterMock.Verify(x => x.ChangeUserPassword(It.IsAny()), Times.Once); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenPasswordChangeFails() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "request-id"); +// var context = new RadiusPipelineContext +// { +// Passphrase = new UserPassphrase { Raw = "newPass1" }, +// ClientConfiguration = new ClientConfiguration { MultifactorSharedSecret = "secret" }, +// LdapProfile = new LdapProfile { Dn = "cn=user,dc=test" }, +// LdapConfiguration = new LdapServerConfiguration +// { +// ConnectionString = "ldap://test", +// Username = "admin", +// Password = "adminPass", +// BindTimeoutSeconds = 30 +// }, +// LdapSchema = It.IsAny(), +// ResponseInformation = new ResponseInformation() +// }; +// +// var encryptedPassword = ProtectionService.Protect("secret", "newPass1"); +// var passwordChangeRequest = new PasswordChangeCache +// { +// Domain = "cache-id", +// NewPasswordEncryptedData = encryptedPassword +// }; +// +// _cacheMock.Setup(x => x.TryGetValue(identifier.RequestId, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out PasswordChangeCache value) => value = passwordChangeRequest)) +// .Returns(true); +// +// _ldapAdapterMock.Setup(x => x.ChangeUserPassword(It.IsAny())) +// .Returns(false); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); +// _cacheMock.Verify(x => x.Remove("cache-id"), Times.Once); +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/SecondFactorChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/SecondFactorChallengeProcessorTests.cs new file mode 100644 index 00000000..0a262445 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/SecondFactorChallengeProcessorTests.cs @@ -0,0 +1,548 @@ +// using System.Text; +// using Microsoft.Extensions.Caching.Memory; +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +// using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.AccessChallenge +// { +// public class SecondFactorChallengeProcessorTests +// { +// private readonly Mock _memoryCacheMock; +// private readonly Mock _apiServiceMock; +// private readonly Mock _ldapAdapterMock; +// private readonly Mock> _loggerMock; +// private readonly SecondFactorChallengeProcessor _processor; +// +// public SecondFactorChallengeProcessorTests() +// { +// _memoryCacheMock = new Mock(); +// _apiServiceMock = new Mock(); +// _ldapAdapterMock = new Mock(); +// _loggerMock = new Mock>(); +// _processor = new SecondFactorChallengeProcessor( +// _apiServiceMock.Object, +// _ldapAdapterMock.Object, +// _loggerMock.Object, +// _memoryCacheMock.Object); +// } +// +// [Fact] +// public void ChallengeType_ShouldReturnSecondFactor() +// { +// // Assert +// Assert.Equal(ChallengeType.SecondFactor, _processor.ChallengeType); +// } +// +// [Fact] +// public void Constructor_ShouldThrowArgumentNullException_WhenMemoryCacheIsNull() +// { +// // Act & Assert +// Assert.Throws(() => +// new SecondFactorChallengeProcessor( +// _apiServiceMock.Object, +// _ldapAdapterMock.Object, +// _loggerMock.Object, +// null)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Act & Assert +// Assert.Throws(() => _processor.AddChallengeContext(null)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldThrowArgumentException_WhenStateIsNullOrWhiteSpace() +// { +// // Arrange +// var context = It.IsAny(); +// +// // Act & Assert +// Assert.Throws(() => _processor.AddChallengeContext(context)); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldAddContextToCacheAndReturnIdentifier() +// { +// // Arrange +// var state = "test-state-123"; +// var context = new RadiusPipelineContext +// { +// ClientConfiguration = new ClientConfiguration { Name = "TestClient" }, +// ResponseInformation = new ResponseInformation { State = state }, +// RequestPacket = new RadiusPacket { Identifier = 123 } +// }; +// +// object cacheEntry = null; +// var cacheKey = $"Challenge:TestClient:{state}"; +// +// _memoryCacheMock.Setup(x => x.Set( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Callback((key, value, options) => +// { +// cacheEntry = value; +// }); +// +// // Act +// var identifier = _processor.AddChallengeContext(context); +// +// // Assert +// Assert.Equal("TestClient", identifier.ClientId); +// Assert.Equal(state, identifier.RequestId); +// _memoryCacheMock.Verify(x => x.Set( +// It.Is(k => k == cacheKey), +// It.Is(c => c == context), +// It.IsAny()), Times.Once); +// } +// +// [Fact] +// public void AddChallengeContext_ShouldReturnEmptyIdentifier_WhenCacheFails() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// ClientConfiguration = new ClientConfiguration { Name = "TestClient" }, +// ResponseInformation = new ResponseInformation { State = "state" }, +// RequestPacket = new RadiusPacket { Identifier = 123 } +// }; +// +// _memoryCacheMock.Setup(x => x.Set( +// It.IsAny(), +// It.IsAny(), +// It.IsAny())) +// .Throws(new Exception("Cache error")); +// +// // Act +// var identifier = _processor.AddChallengeContext(context); +// +// // Assert +// Assert.Equal(ChallengeIdentifier.Empty, identifier); +// } +// +// [Fact] +// public void HasChallengeContext_ShouldReturnTrue_WhenContextExistsInCache() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("TestClient", "state-123"); +// var cacheKey = $"Challenge:TestClient:state-123"; +// +// object cachedValue = new object(); +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = cachedValue)) +// .Returns(true); +// +// // Act +// var result = _processor.HasChallengeContext(identifier); +// +// // Assert +// Assert.True(result); +// } +// +// [Fact] +// public void HasChallengeContext_ShouldReturnFalse_WhenContextNotInCache() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("TestClient", "state-123"); +// var cacheKey = $"Challenge:TestClient:state-123"; +// +// object cachedValue = null; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = null)) +// .Returns(false); +// +// // Act +// var result = _processor.HasChallengeContext(identifier); +// +// // Assert +// Assert.False(result); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenUserNameIsEmpty() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "", +// Identifier = 1, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// ResponseInformation = new ResponseInformation() +// }; +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// Assert.Equal("state", context.ResponseInformation.State); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenPAPAuthenticationWithEmptyPassword() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.PAP, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// Passphrase = new UserPassphrase { Raw = "" }, +// ResponseInformation = new ResponseInformation() +// }; +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// Assert.Equal("state", context.ResponseInformation.State); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenMSCHAP2WithoutResponse() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.MSCHAP2, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// ResponseInformation = new ResponseInformation() +// }; +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// Assert.Equal("state", context.ResponseInformation.State); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenUnsupportedAuthenticationType() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = (AuthenticationType)999, // Invalid type +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// ResponseInformation = new ResponseInformation() +// }; +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// Assert.Equal("state", context.ResponseInformation.State); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldThrowInvalidOperationException_WhenContextNotFound() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.PAP, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// Passphrase = new UserPassphrase { Raw = "password123" }, +// ResponseInformation = new ResponseInformation() +// }; +// +// var cacheKey = $"Challenge:client:state"; +// object cachedValue = null; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = null)) +// .Returns(false); +// +// // Act & Assert +// await Assert.ThrowsAsync(() => +// _processor.ProcessChallengeAsync(identifier, context)); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldProcessPAPAuthentication() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var challengeContext = new RadiusPipelineContext(); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.PAP, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// Passphrase = new UserPassphrase { Raw = "password123" }, +// ResponseInformation = new ResponseInformation() +// }; +// +// var cacheKey = $"Challenge:client:state"; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = challengeContext)) +// .Returns(true); +// +// var apiResponse = new SecondFactorResponse +// { +// Code = AuthenticationStatus.Accept, +// ReplyMessage = "Accepted" +// }; +// +// _apiServiceMock.Setup(x => x.SendChallengeAsync( +// challengeContext, +// It.IsAny(), +// identifier.RequestId, +// "password123")) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Accept, result); +// Assert.Equal("Accepted", context.ResponseInformation.ReplyMessage); +// Assert.Equal(AuthenticationStatus.Accept, context.SecondFactorStatus); +// _memoryCacheMock.Verify(x => x.Remove(cacheKey), Times.Once); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldProcessMSCHAP2Authentication() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var challengeContext = new RadiusPipelineContext(); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.MSCHAP2, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// ResponseInformation = new ResponseInformation() +// }; +// +// var otpBytes = Encoding.ASCII.GetBytes("123456"); +// var msChapResponse = new byte[] { 0x00, 0x00 }.Concat(otpBytes).ToArray(); +// +// var mockRequestPacket = new Mock(); +// mockRequestPacket.Setup(x => x.GetAttribute("MS-CHAP2-Response")) +// .Returns(msChapResponse); +// mockRequestPacket.Setup(x => x.UserName).Returns("testuser"); +// mockRequestPacket.Setup(x => x.Identifier).Returns(1); +// mockRequestPacket.Setup(x => x.AuthenticationType).Returns(AuthenticationType.MSCHAP2); +// mockRequestPacket.Setup(x => x.RemoteEndpoint).Returns(new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812)); +// +// context.RequestPacket = mockRequestPacket.Object; +// +// var cacheKey = $"Challenge:client:state"; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = challengeContext)) +// .Returns(true); +// +// var apiResponse = new SecondFactorResponse +// { +// Code = AuthenticationStatus.Accept, +// ReplyMessage = "Accepted" +// }; +// +// _apiServiceMock.Setup(x => x.SendChallengeAsync( +// challengeContext, +// It.IsAny(), +// identifier.RequestId, +// "123456")) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Accept, result); +// Assert.Equal("Accepted", context.ResponseInformation.ReplyMessage); +// Assert.Equal(AuthenticationStatus.Accept, context.SecondFactorStatus); +// _memoryCacheMock.Verify(x => x.Remove(cacheKey), Times.Once); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnInProcess_WhenApiReturnsOtherStatus() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var challengeContext = new RadiusPipelineContext(); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.PAP, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// Passphrase = new UserPassphrase { Raw = "password123" }, +// ResponseInformation = new ResponseInformation() +// }; +// +// var cacheKey = $"Challenge:client:state"; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = challengeContext)) +// .Returns(true); +// +// var apiResponse = new SecondFactorResponse +// { +// Code = AuthenticationStatus.Challenge, +// ReplyMessage = "Continue" +// }; +// +// _apiServiceMock.Setup(x => x.SendChallengeAsync( +// challengeContext, +// It.IsAny(), +// identifier.RequestId, +// "password123")) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.InProcess, result); +// Assert.Equal("Continue", context.ResponseInformation.ReplyMessage); +// Assert.Equal("state", context.ResponseInformation.State); +// } +// +// [Fact] +// public async Task ProcessChallengeAsync_ShouldReturnReject_WhenApiReturnsReject() +// { +// // Arrange +// var identifier = new ChallengeIdentifier("client", "state"); +// var challengeContext = new RadiusPipelineContext(); +// var context = new RadiusPipelineContext +// { +// RequestPacket = new RadiusPacket +// { +// UserName = "testuser", +// Identifier = 1, +// AuthenticationType = AuthenticationType.PAP, +// RemoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 1812) +// }, +// Passphrase = new UserPassphrase { Raw = "password123" }, +// ResponseInformation = new ResponseInformation() +// }; +// +// var cacheKey = $"Challenge:client:state"; +// _memoryCacheMock.Setup(x => x.TryGetValue(cacheKey, out It.Ref.IsAny)) +// .Callback(new TryGetValueCallback((string key, out object value) => value = challengeContext)) +// .Returns(true); +// +// var apiResponse = new SecondFactorResponse +// { +// Code = AuthenticationStatus.Reject, +// ReplyMessage = "Rejected" +// }; +// +// _apiServiceMock.Setup(x => x.SendChallengeAsync( +// challengeContext, +// It.IsAny(), +// identifier.RequestId, +// "password123")) +// .ReturnsAsync(apiResponse); +// +// // Act +// var result = await _processor.ProcessChallengeAsync(identifier, context); +// +// // Assert +// Assert.Equal(ChallengeStatus.Reject, result); +// Assert.Equal("Rejected", context.ResponseInformation.ReplyMessage); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// Assert.Equal("state", context.ResponseInformation.State); +// _memoryCacheMock.Verify(x => x.Remove(cacheKey), Times.Once); +// } +// +// [Fact] +// public void ShouldCacheResponse_ShouldReturnTrue_WhenNoLdapConfiguration() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = null, +// RequestPacket = new RadiusPacket { UserName = "testuser" } +// }; +// +// // Act +// var result = _processor.GetType().GetMethod("ShouldCacheResponse", +// System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) +// .Invoke(_processor, new object[] { context }); +// +// // Assert +// Assert.True((bool)result); +// } +// +// [Fact] +// public void ShouldCacheResponse_ShouldReturnTrue_WhenNoAuthenticationCacheGroups() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = new LdapServerConfiguration() +// { +// AuthenticationCacheGroups = new List() +// }, +// RequestPacket = new RadiusPacket() { UserName = "testuser" } +// }; +// +// // Act +// var result = _processor.GetType().GetMethod("ShouldCacheResponse", +// System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) +// .Invoke(_processor, new object[] { context }); +// +// // Assert +// Assert.True((bool)result); +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatterTests.cs new file mode 100644 index 00000000..5ac14dec --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatterTests.cs @@ -0,0 +1,65 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class ActiveDirectoryFormatterTests + { + private readonly ActiveDirectoryFormatter _formatter; + + public ActiveDirectoryFormatterTests() + { + _formatter = new ActiveDirectoryFormatter(); + } + + [Fact] + public void LdapImplementation_ShouldBeActiveDirectory() + { + // Act & Assert + Assert.Equal(LdapImplementation.ActiveDirectory, _formatter.LdapImplementation); + } + + [Theory] + [InlineData("user@domain.com")] // UPN + [InlineData("DOMAIN\\user")] // NetBIOS + [InlineData("user")] // sAMAccountName + [InlineData("CN=User,OU=Users,DC=domain,DC=com")] // DN + public void FormatName_ShouldReturnOriginalName(string userName) + { + // Arrange + var profile = new MockLdapProfile(); + + // Act + var result = _formatter.FormatName(userName, profile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldHandleNullProfile() + { + // Arrange + var userName = "testuser"; + + // Act + var result = _formatter.FormatName(userName, null); + + // Assert + Assert.Equal(userName, result); + } + + private class MockLdapProfile : ILdapProfile + { + public DistinguishedName Dn { get; } + public string? Phone { get; } + public string? Email { get; } + public string? DisplayName { get; } + public IReadOnlyCollection MemberOf { get; } + public IReadOnlyCollection Attributes { get; } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatterTests.cs new file mode 100644 index 00000000..b8f32d7a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatterTests.cs @@ -0,0 +1,140 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class FreeIpaFormatterTests + { + private readonly FreeIpaFormatter _formatter; + + public FreeIpaFormatterTests() + { + _formatter = new FreeIpaFormatter(); + } + + [Fact] + public void LdapImplementation_ShouldReturnFreeIPA() + { + // Assert + Assert.Equal(LdapImplementation.FreeIPA, _formatter.LdapImplementation); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenUserPrincipalNameFormat() + { + // Arrange + var userName = "user@domain.com"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenDistinguishedNameFormat() + { + // Arrange + var userName = "cn=user,ou=users,dc=test"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenSimpleNameFormat() + { + // Arrange + var userName = "simpleuser"; + var ldapProfile = new MockLdapProfile("cn=simpleuser,ou=users,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=simpleuser,ou=users,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenNetBiosNameFormat() + { + // Arrange + var userName = "DOMAIN\\user"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=domain,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=domain,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenEmailFormat() + { + // Arrange + var userName = "user@test.local"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=test,dc=local"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=test,dc=local", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenUnsupportedFormat() + { + // Arrange + var userName = "just-a-string"; + var ldapProfile = new MockLdapProfile("cn=just-a-string,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=just-a-string,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnEmptyString_WhenLdapProfileDnIsEmpty() + { + // Arrange + var userName = "user"; + var ldapProfile = new MockLdapProfile(""); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("", result); + } + + private class MockLdapProfile : ILdapProfile + { + private readonly string _dn; + + public MockLdapProfile(string dn) + { + _dn = dn; + } + + public DistinguishedName Dn => new DistinguishedName(_dn); + public string? Phone { get; } + public string? Email { get; } + public string? DisplayName { get; } + public IReadOnlyCollection MemberOf { get; } + public IReadOnlyCollection Attributes { get; } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProviderTests.cs new file mode 100644 index 00000000..a94ca3f1 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProviderTests.cs @@ -0,0 +1,63 @@ +using Moq; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class LdapBindNameFormatterProviderTests + { + [Fact] + public void GetLdapBindNameFormatter_ShouldReturnCorrectFormatter() + { + // Arrange + var adFormatter = new Mock(); + adFormatter.Setup(x => x.LdapImplementation).Returns(LdapImplementation.ActiveDirectory); + + var openLdapFormatter = new Mock(); + openLdapFormatter.Setup(x => x.LdapImplementation).Returns(LdapImplementation.OpenLDAP); + + var formatters = new List + { + adFormatter.Object, + openLdapFormatter.Object + }; + + var provider = new LdapBindNameFormatterProvider(formatters); + + // Act + var result = provider.GetLdapBindNameFormatter(LdapImplementation.OpenLDAP); + + // Assert + Assert.Equal(openLdapFormatter.Object, result); + } + + [Fact] + public void GetLdapBindNameFormatter_ShouldReturnNullWhenNotFound() + { + // Arrange + var formatter = new Mock(); + formatter.Setup(x => x.LdapImplementation).Returns(LdapImplementation.ActiveDirectory); + + var provider = new LdapBindNameFormatterProvider(new[] { formatter.Object }); + + // Act + var result = provider.GetLdapBindNameFormatter(LdapImplementation.OpenLDAP); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLdapBindNameFormatter_ShouldHandleEmptyFormatters() + { + // Arrange + var provider = new LdapBindNameFormatterProvider(new List()); + + // Act + var result = provider.GetLdapBindNameFormatter(LdapImplementation.ActiveDirectory); + + // Assert + Assert.Null(result); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatterTests.cs new file mode 100644 index 00000000..faec599f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatterTests.cs @@ -0,0 +1,126 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class MultiDirectoryFormatterTests + { + private readonly MultiDirectoryFormatter _formatter; + + public MultiDirectoryFormatterTests() + { + _formatter = new MultiDirectoryFormatter(); + } + + [Fact] + public void LdapImplementation_ShouldReturnMultiDirectory() + { + // Assert + Assert.Equal(LdapImplementation.MultiDirectory, _formatter.LdapImplementation); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenUserPrincipalNameFormat() + { + // Arrange + var userName = "user@domain.com"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenDistinguishedNameFormat() + { + // Arrange + var userName = "cn=user,ou=users,dc=test"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenSimpleNameFormat() + { + // Arrange + var userName = "simpleuser"; + var ldapProfile = new MockLdapProfile("cn=simpleuser,ou=users,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=simpleuser,ou=users,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenNetBiosNameFormat() + { + // Arrange + var userName = "DOMAIN\\user"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=domain,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=domain,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenEmailFormat() + { + // Arrange + var userName = "user@test.local"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=test,dc=local"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=test,dc=local", result); + } + + [Fact] + public void FormatName_ShouldReturnEmptyString_WhenLdapProfileDnIsEmpty() + { + // Arrange + var userName = "user"; + var ldapProfile = new MockLdapProfile(""); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("", result); + } + + private class MockLdapProfile : ILdapProfile + { + private readonly string _dn; + + public MockLdapProfile(string dn) + { + _dn = dn; + } + + public DistinguishedName Dn => new DistinguishedName(_dn); + public string? Phone { get; } + public string? Email { get; } + public string? DisplayName { get; } + public IReadOnlyCollection MemberOf { get; } + public IReadOnlyCollection Attributes { get; } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatterTests.cs new file mode 100644 index 00000000..4c5cfd3f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatterTests.cs @@ -0,0 +1,126 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class OpenLdapFormatterTests + { + private readonly OpenLdapFormatter _formatter; + + public OpenLdapFormatterTests() + { + _formatter = new OpenLdapFormatter(); + } + + [Fact] + public void LdapImplementation_ShouldReturnOpenLDAP() + { + // Assert + Assert.Equal(LdapImplementation.OpenLDAP, _formatter.LdapImplementation); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenUserPrincipalNameFormat() + { + // Arrange + var userName = "user@domain.com"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenDistinguishedNameFormat() + { + // Arrange + var userName = "cn=user,ou=users,dc=test"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenSimpleNameFormat() + { + // Arrange + var userName = "simpleuser"; + var ldapProfile = new MockLdapProfile("cn=simpleuser,ou=users,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=simpleuser,ou=users,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenNetBiosNameFormat() + { + // Arrange + var userName = "DOMAIN\\user"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=domain,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=domain,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenEmailFormat() + { + // Arrange + var userName = "user@test.local"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=test,dc=local"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=test,dc=local", result); + } + + [Fact] + public void FormatName_ShouldReturnEmptyString_WhenLdapProfileDnIsEmpty() + { + // Arrange + var userName = "user"; + var ldapProfile = new MockLdapProfile(""); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("", result); + } + + private class MockLdapProfile : ILdapProfile + { + private readonly string _dn; + + public MockLdapProfile(string dn) + { + _dn = dn; + } + + public DistinguishedName Dn => new DistinguishedName(_dn); + public string? Phone { get; } + public string? Email { get; } + public string? DisplayName { get; } + public IReadOnlyCollection MemberOf { get; } + public IReadOnlyCollection Attributes { get; } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/SambaFormatterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/SambaFormatterTests.cs new file mode 100644 index 00000000..8a2b59a0 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/SambaFormatterTests.cs @@ -0,0 +1,126 @@ +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor.BindNameFormat +{ + public class SambaFormatterTests + { + private readonly SambaFormatter _formatter; + + public SambaFormatterTests() + { + _formatter = new SambaFormatter(); + } + + [Fact] + public void LdapImplementation_ShouldReturnSamba() + { + // Assert + Assert.Equal(LdapImplementation.Samba, _formatter.LdapImplementation); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenUserPrincipalNameFormat() + { + // Arrange + var userName = "user@domain.com"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnOriginal_WhenDistinguishedNameFormat() + { + // Arrange + var userName = "cn=user,ou=users,dc=test"; + var ldapProfile = new MockLdapProfile("cn=user,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal(userName, result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenSimpleNameFormat() + { + // Arrange + var userName = "simpleuser"; + var ldapProfile = new MockLdapProfile("cn=simpleuser,ou=users,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=simpleuser,ou=users,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenNetBiosNameFormat() + { + // Arrange + var userName = "DOMAIN\\user"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=domain,dc=test"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=domain,dc=test", result); + } + + [Fact] + public void FormatName_ShouldReturnLdapProfileDn_WhenEmailFormat() + { + // Arrange + var userName = "user@test.local"; + var ldapProfile = new MockLdapProfile("cn=user,ou=users,dc=test,dc=local"); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("cn=user,ou=users,dc=test,dc=local", result); + } + + [Fact] + public void FormatName_ShouldReturnEmptyString_WhenLdapProfileDnIsEmpty() + { + // Arrange + var userName = "user"; + var ldapProfile = new MockLdapProfile(""); + + // Act + var result = _formatter.FormatName(userName, ldapProfile); + + // Assert + Assert.Equal("", result); + } + + private class MockLdapProfile : ILdapProfile + { + private readonly string _dn; + + public MockLdapProfile(string dn) + { + _dn = dn; + } + + public DistinguishedName Dn => new DistinguishedName(_dn); + public string? Phone { get; } + public string? Email { get; } + public string? DisplayName { get; } + public IReadOnlyCollection MemberOf { get; } + public IReadOnlyCollection Attributes { get; } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/FirstFactorProcessorProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/FirstFactorProcessorProviderTests.cs new file mode 100644 index 00000000..17dfee35 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/FirstFactorProcessorProviderTests.cs @@ -0,0 +1,72 @@ +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor +{ + public class FirstFactorProcessorProviderTests + { + [Fact] + public void Constructor_ShouldThrowWhenProcessorsNull() + { + // Act & Assert + Assert.Throws(() => new FirstFactorProcessorProvider(null)); + } + + [Fact] + public void GetProcessor_ShouldReturnCorrectProcessor() + { + // Arrange + var radiusProcessor = new Mock(); + radiusProcessor.Setup(x => x.AuthenticationSource).Returns(AuthenticationSource.Radius); + + var ldapProcessor = new Mock(); + ldapProcessor.Setup(x => x.AuthenticationSource).Returns(AuthenticationSource.Ldap); + + var processors = new[] { radiusProcessor.Object, ldapProcessor.Object }; + var provider = new FirstFactorProcessorProvider(processors); + + // Act + var result = provider.GetProcessor(AuthenticationSource.Ldap); + + // Assert + Assert.Equal(ldapProcessor.Object, result); + } + + [Fact] + public void GetProcessor_ShouldThrowWhenProcessorNotFound() + { + // Arrange + var processor = new Mock(); + processor.Setup(x => x.AuthenticationSource).Returns(AuthenticationSource.Radius); + + var provider = new FirstFactorProcessorProvider(new[] { processor.Object }); + + // Act & Assert + var exception = Assert.Throws( + () => provider.GetProcessor(AuthenticationSource.Ldap)); + + Assert.Contains("No processor found", exception.Message); + } + + [Fact] + public void GetProcessor_ShouldHandleMultipleProcessorsWithSameSource() + { + // Arrange + var processor1 = new Mock(); + processor1.Setup(x => x.AuthenticationSource).Returns(AuthenticationSource.Radius); + + var processor2 = new Mock(); + processor2.Setup(x => x.AuthenticationSource).Returns(AuthenticationSource.Radius); + + var provider = new FirstFactorProcessorProvider(new[] { processor1.Object, processor2.Object }); + + // Act + var result = provider.GetProcessor(AuthenticationSource.Radius); + + // Assert - должен вернуть первый найденный + Assert.NotNull(result); + Assert.Equal(AuthenticationSource.Radius, result.AuthenticationSource); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs new file mode 100644 index 00000000..707a9c5c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs @@ -0,0 +1,252 @@ +using System.DirectoryServices.Protocols; +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor +{ + public class LdapFirstFactorProcessorTests + { + private readonly Mock _formatterProviderMock; + private readonly Mock> _loggerMock; + private readonly Mock _ldapAdapterMock; + private readonly LdapFirstFactorProcessor _processor; + + public LdapFirstFactorProcessorTests() + { + _formatterProviderMock = new Mock(); + _loggerMock = new Mock>(); + _ldapAdapterMock = new Mock(); + + _processor = new LdapFirstFactorProcessor( + _formatterProviderMock.Object, + _loggerMock.Object, + _ldapAdapterMock.Object); + } + + [Fact] + public void AuthenticationSource_ShouldBeLdap() + { + // Act & Assert + Assert.Equal(AuthenticationSource.Ldap, _processor.AuthenticationSource); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenUserNameMissing() + { + // Arrange + var requestPacket = CreateRadiusPacket(userName: null); + var context = CreateContext(requestPacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Can't find User-Name")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenPasswordMissing() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + context.Passphrase = UserPassphrase.Parse("", PreAuthMode.None); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No User-Password")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldAcceptWhenLdapBindSucceeds() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + _ldapAdapterMock + .Setup(x => x.CheckConnection(It.IsAny())) + .Returns(true); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("verified successfully")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenLdapBindFails() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + _ldapAdapterMock + .Setup(x => x.CheckConnection(It.IsAny())) + .Returns(false); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldHandleLdapException() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + var ldapException = new LdapException(52, "Bind failed", "data 52e"); + + _ldapAdapterMock + .Setup(x => x.CheckConnection(It.IsAny())) + .Throws(ldapException); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("InvalidCredentials")), + It.IsAny(), + It.IsAny>()!), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldSetMustChangePasswordOnSpecificErrors() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + var ldapException = new LdapException(532 ,"Password expired", "data 532"); + + _ldapAdapterMock + .Setup(x => x.CheckConnection(It.IsAny())) + .Throws(ldapException); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + Assert.Equal(context.LdapConfiguration.ConnectionString, context.MustChangePasswordDomain); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldThrowWhenLdapConfigurationMissing() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContextWithoutLdap(requestPacket); + + // Act & Assert + await Assert.ThrowsAsync( + () => _processor.ProcessFirstFactor(context)); + } + + private RadiusPacket CreateRadiusPacket(string userName) + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Any, 228), + }; + + if (userName != null) + { + packet.AddAttributeValue("User-Name", userName); + packet.AddAttributeValue("User-Password", "password"); + } + + return packet; + } + + private RadiusPipelineContext CreateContext(RadiusPacket requestPacket) + { + var clientConfig = new ClientConfiguration + { + Name = "TestClient" + }; + + var ldapConfig = new LdapServerConfiguration + { + ConnectionString = "ldap://test.domain.com", + Username = "admin", + Password = "admin-pass", + BindTimeoutSeconds = 30 + }; + + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig) + { + Passphrase = UserPassphrase.Parse("password", PreAuthMode.None), + LdapSchema = LdapSchemaBuilder.Default + }; + + return context; + } + + private RadiusPipelineContext CreateContextWithoutLdap(RadiusPacket requestPacket) + { + var clientConfig = new ClientConfiguration + { + Name = "TestClient" + }; + + var context = new RadiusPipelineContext(requestPacket, clientConfig) + { + Passphrase = UserPassphrase.Parse("password", PreAuthMode.None), + LdapSchema = LdapSchemaBuilder.Default + }; + + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs new file mode 100644 index 00000000..45c7b413 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs @@ -0,0 +1,91 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor +{ + public class NoneFirstFactorProcessorTests + { + private readonly Mock> _loggerMock; + private readonly NoneFirstFactorProcessor _processor; + + public NoneFirstFactorProcessorTests() + { + _loggerMock = new Mock>(); + _processor = new NoneFirstFactorProcessor(_loggerMock.Object); + } + + [Fact] + public void AuthenticationSource_ShouldBeNone() + { + // Act & Assert + Assert.Equal(AuthenticationSource.None, _processor.AuthenticationSource); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldAlwaysAccept() + { + // Arrange + var requestPacket = CreateRadiusPacket(); + var context = CreateContext(requestPacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Bypass first factor")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldAcceptEvenWithEmptyUserName() + { + // Arrange + var requestPacket = CreateRadiusPacket(userName: null); + var context = CreateContext(requestPacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + } + + private RadiusPacket CreateRadiusPacket(string userName = "testuser") + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header); + + if (userName != null) + { + packet.AddAttributeValue("User-Name", userName); + } + + return packet; + } + + private RadiusPipelineContext CreateContext(RadiusPacket requestPacket) + { + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + FirstFactorAuthenticationSource = AuthenticationSource.None + }; + + return new RadiusPipelineContext(requestPacket, clientConfig); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs new file mode 100644 index 00000000..9d15053f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs @@ -0,0 +1,265 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor +{ + public class RadiusFirstFactorProcessorTests + { + private readonly Mock _radiusPacketServiceMock; + private readonly Mock> _loggerMock; + private readonly Mock _radiusClientMock; + private readonly RadiusFirstFactorProcessor _processor; + + public RadiusFirstFactorProcessorTests() + { + _radiusPacketServiceMock = new Mock(); + var radiusClientFactoryMock = new Mock(); + _loggerMock = new Mock>(); + _radiusClientMock = new Mock(); + + _processor = new RadiusFirstFactorProcessor( + _radiusPacketServiceMock.Object, + radiusClientFactoryMock.Object, + _loggerMock.Object); + + radiusClientFactoryMock + .Setup(x => x.CreateRadiusClient(It.IsAny())) + .Returns(_radiusClientMock.Object); + } + + [Fact] + public void AuthenticationSource_ShouldBeRadius() + { + // Act & Assert + Assert.Equal(AuthenticationSource.Radius, _processor.AuthenticationSource); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenUserNameMissing() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var requestPacket = CreateRadiusPacket(userName: null); + var context = CreateContext(requestPacket, clientConfiguration); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Can't find User-Name")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldAcceptWhenRadiusAccepts() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateContext(requestPacket, clientConfiguration); + + var responsePacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); + + SetupSuccessfulRadiusCall(responsePacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + Assert.Equal(responsePacket, context.ResponsePacket); + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("verified successfully")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenRadiusRejects() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateContext(requestPacket, clientConfiguration); + + var responsePacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessReject, 1, new byte[16])); + + SetupSuccessfulRadiusCall(responsePacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldTryNextServerWhenFirstFails() + { + // Arrange + var npsServerEndpoints = new[] + { + new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812), + new IPEndPoint(IPAddress.Parse("192.168.1.2"), 1812) + }; + var requestPacket = CreateRadiusPacket("testuser"); + var clientConfiguration = CreateTestClientConfiguration(npsServerEndpoints); + var context = CreateContext(requestPacket, clientConfiguration); + + + + var responsePacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); + + // Первый сервер не отвечает, второй отвечает успешно + _radiusClientMock + .SetupSequence(x => x.SendPacketAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((byte[])null) + .ReturnsAsync(new byte[100]); + + _radiusPacketServiceMock + .Setup(x => x.SerializePacket(It.IsAny(), It.IsAny())) + .Returns(new byte[100]); + + _radiusPacketServiceMock + .Setup(x => x.ParsePacket(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(responsePacket); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("did not respond")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ProcessFirstFactor_ShouldRejectWhenAllServersFail() + { + // Arrange + var npsServerEndpoints = new[] + { + new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812), + new IPEndPoint(IPAddress.Parse("192.168.1.2"), 1812) + }; + var requestPacket = CreateRadiusPacket("testuser"); + var clientConfiguration = CreateTestClientConfiguration(npsServerEndpoints); + var context = CreateContext(requestPacket, clientConfiguration); + + _radiusClientMock + .Setup(x => x.SendPacketAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((byte[])null); + + // Act + await _processor.ProcessFirstFactor(context); + + // Assert + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("did not respond")), + It.IsAny(), + It.IsAny>()), + Times.Exactly(2)); + } + + private void SetupSuccessfulRadiusCall(RadiusPacket responsePacket) + { + _radiusClientMock + .Setup(x => x.SendPacketAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new byte[100]); + + _radiusPacketServiceMock + .Setup(x => x.SerializePacket(It.IsAny(), It.IsAny())) + .Returns(new byte[100]); + + _radiusPacketServiceMock + .Setup(x => x.ParsePacket(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(responsePacket); + } + + private RadiusPacket CreateRadiusPacket(string userName) + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Any, 228), + }; + + if (userName != null) + { + packet.AddAttributeValue("User-Name", userName); + packet.AddAttributeValue("User-Password", "password"); + } + + return packet; + } + + private static ClientConfiguration CreateTestClientConfiguration(IPEndPoint[]? npsServerEndpoints = null) + { + return new ClientConfiguration + { + Name = "TestClient", + RadiusSharedSecret = "shared-secret", + AdapterClientEndpoint = new IPEndPoint(IPAddress.Any, 0), + NpsServerEndpoints = npsServerEndpoints ?? [new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812)], + NpsServerTimeout = TimeSpan.FromSeconds(5) + }; + } + + private static RadiusPipelineContext CreateContext(RadiusPacket requestPacket, ClientConfiguration clientConfig) + { + + + var context = new RadiusPipelineContext(requestPacket, clientConfig) + { + Passphrase = UserPassphrase.Parse("password", PreAuthMode.None) + }; + + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineFactoryTests.cs new file mode 100644 index 00000000..5d1f515b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineFactoryTests.cs @@ -0,0 +1,203 @@ +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Pipeline +// { +// public class RadiusPipelineFactoryTests +// { +// private readonly Mock _serviceProviderMock; +// private readonly Mock> _loggerMock; +// private readonly RadiusPipelineFactory _factory; +// +// public RadiusPipelineFactoryTests() +// { +// _serviceProviderMock = new Mock(); +// _loggerMock = new Mock>(); +// _factory = new RadiusPipelineFactory(_serviceProviderMock.Object, _loggerMock.Object); +// +// SetupDefaultStepMocks(); +// } +// +// [Fact] +// public void CreatePipeline_ShouldCreateBasicStepsForEmptyConfig() +// { +// // Arrange +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient", +// PreAuthenticationMethod = null, +// ReplyAttributes = new Dictionary() +// }; +// +// // Act +// var pipeline = _factory.CreatePipeline(clientConfig); +// +// // Assert +// Assert.NotNull(pipeline); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// } +// +// [Fact] +// public void CreatePipeline_ShouldAddLdapStepsWhenLdapConfigured() +// { +// // Arrange +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient", +// LdapServers = new List { new() }, +// PreAuthenticationMethod = null, +// ReplyAttributes = new Dictionary() +// }; +// +// // Act +// _factory.CreatePipeline(clientConfig); +// +// // Assert +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// } +// +// [Fact] +// public void CreatePipeline_ShouldAddPreAuthStepsWhenPreAuthEnabled() +// { +// // Arrange +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient", +// LdapServers = new List { new() }, +// PreAuthenticationMethod = PreAuthMode.Any, +// ReplyAttributes = new Dictionary() +// }; +// +// // Act +// _factory.CreatePipeline(clientConfig); +// +// // Assert +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// VerifyStepCreated(Times.Once()); +// } +// +// [Fact] +// public void CreatePipeline_ShouldAddUserGroupLoadingStepWhenRequired() +// { +// // Arrange +// var replyAttributes = new Dictionary +// { +// ["MemberOf"] = new[] { new RadiusReplyAttribute { Name = "memberOf" } } +// }; +// +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient", +// LdapServers = new List { new() }, +// PreAuthenticationMethod = null, +// ReplyAttributes = replyAttributes +// }; +// +// // Act +// _factory.CreatePipeline(clientConfig); +// +// // Assert +// VerifyStepCreated(Times.Once()); +// } +// +// [Fact] +// public void CreatePipeline_ShouldNotAddUserGroupLoadingStepWhenNotRequired() +// { +// // Arrange +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient", +// LdapServers = new List { new() }, +// PreAuthenticationMethod = null, +// ReplyAttributes = new Dictionary +// { +// ["Class"] = new[] { new RadiusReplyAttribute { Value = "Test" } } +// } +// }; +// +// // Act +// _factory.CreatePipeline(clientConfig); +// +// // Assert +// VerifyStepCreated(Times.Never()); +// } +// +// [Fact] +// public void CreatePipeline_ShouldLogPipelineCreation() +// { +// // Arrange +// var clientConfig = new ClientConfiguration +// { +// Name = "TestClient" +// }; +// +// // Act +// _factory.CreatePipeline(clientConfig); +// +// // Assert +// _loggerMock.Verify( +// x => x.Log( +// LogLevel.Debug, +// It.IsAny(), +// It.Is((v, t) => v.ToString().Contains("Configuration: TestClient")), +// It.IsAny(), +// It.IsAny>()), +// Times.Once); +// } +// +// private void SetupDefaultStepMocks() +// { +// var steps = new Mock[] +// { +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock(), +// new Mock() +// }; +// _serviceProviderMock +// .Setup(x => x.GetService()) +// .Returns(type => +// { +// return new Mock(); +// }); +// // foreach (var step in steps) +// // { +// // _serviceProviderMock +// // .Setup(x => x.GetService(step.Object.GetType())) +// // .Returns(step.Object); +// // } +// } +// +// private void VerifyStepCreated(Times times) where TStep : IRadiusPipelineStep +// { +// _serviceProviderMock.Verify( +// x => x.GetRequiredService(), +// times); +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs new file mode 100644 index 00000000..d7cf1c7a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs @@ -0,0 +1,113 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline +{ + public class RadiusPipelineProviderTests + { + private readonly Mock _pipelineFactoryMock; + private readonly Mock> _loggerMock; + private readonly RadiusPipelineProvider _provider; + + public RadiusPipelineProviderTests() + { + _pipelineFactoryMock = new Mock(); + _loggerMock = new Mock>(); + _provider = new RadiusPipelineProvider(_pipelineFactoryMock.Object, _loggerMock.Object); + } + + [Fact] + public void GetPipeline_ShouldCreateNewPipelineOnFirstCall() + { + // Arrange + var clientConfig = new ClientConfiguration { Name = "Client1" }; + var expectedPipeline = Mock.Of(); + + _pipelineFactoryMock + .Setup(x => x.CreatePipeline(clientConfig)) + .Returns(expectedPipeline); + + // Act + var pipeline = _provider.GetPipeline(clientConfig); + + // Assert + Assert.Equal(expectedPipeline, pipeline); + _pipelineFactoryMock.Verify(x => x.CreatePipeline(clientConfig), Times.Once); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Creating new pipeline")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public void GetPipeline_ShouldReturnCachedPipelineOnSubsequentCalls() + { + // Arrange + var clientConfig = new ClientConfiguration { Name = "Client1" }; + var expectedPipeline = Mock.Of(); + + _pipelineFactoryMock + .Setup(x => x.CreatePipeline(clientConfig)) + .Returns(expectedPipeline); + + // Act + var pipeline1 = _provider.GetPipeline(clientConfig); + var pipeline2 = _provider.GetPipeline(clientConfig); + var pipeline3 = _provider.GetPipeline(clientConfig); + + // Assert + Assert.Equal(expectedPipeline, pipeline1); + Assert.Equal(expectedPipeline, pipeline2); + Assert.Equal(expectedPipeline, pipeline3); + _pipelineFactoryMock.Verify(x => x.CreatePipeline(clientConfig), Times.Once); + } + + [Fact] + public void GetPipeline_ShouldCacheDifferentClientsSeparately() + { + // Arrange + var client1 = new ClientConfiguration { Name = "Client1" }; + var client2 = new ClientConfiguration { Name = "Client2" }; + + var pipeline1 = Mock.Of(); + var pipeline2 = Mock.Of(); + + _pipelineFactoryMock + .Setup(x => x.CreatePipeline(client1)) + .Returns(pipeline1); + + _pipelineFactoryMock + .Setup(x => x.CreatePipeline(client2)) + .Returns(pipeline2); + + // Act + var result1 = _provider.GetPipeline(client1); + var result2 = _provider.GetPipeline(client2); + var result1Again = _provider.GetPipeline(client1); + + // Assert + Assert.Equal(pipeline1, result1); + Assert.Equal(pipeline2, result2); + Assert.Equal(pipeline1, result1Again); + _pipelineFactoryMock.Verify(x => x.CreatePipeline(client1), Times.Once); + _pipelineFactoryMock.Verify(x => x.CreatePipeline(client2), Times.Once); + } + + [Fact] + public void GetPipeline_ShouldThrowWhenClientNameIsNull() + { + // Arrange + var clientConfig = new ClientConfiguration { Name = null }; + + // Act & Assert + Assert.Throws(() => _provider.GetPipeline(clientConfig)); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs new file mode 100644 index 00000000..057b871b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs @@ -0,0 +1,142 @@ +using System.Net; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline +{ + public class RadiusPipelineTests + { + [Fact] + public async Task ExecuteAsync_ShouldExecuteAllStepsInOrder() + { + // Arrange + var executionOrder = new List(); + var steps = CreateMockSteps(3, executionOrder); + var pipeline = new RadiusPipeline(steps); + var context = new RadiusPipelineContext( + CreateRequestPacket(), + new ClientConfiguration()); + + // Act + await pipeline.ExecuteAsync(context); + + // Assert + Assert.Equal(new[] { "Step1", "Step2", "Step3" }, executionOrder); + } + + [Fact] + public async Task ExecuteAsync_ShouldStopExecutionWhenTerminated() + { + // Arrange + var executionOrder = new List(); + var steps = new List + { + CreateMockStep("Step1", executionOrder, terminate: false), + CreateMockStep("Step2", executionOrder, terminate: true), + CreateMockStep("Step3", executionOrder, terminate: false) // Этот шаг не должен выполниться + }; + + var pipeline = new RadiusPipeline(steps); + var context = new RadiusPipelineContext( + CreateRequestPacket(), + new ClientConfiguration()); + + // Act + await pipeline.ExecuteAsync(context); + + // Assert + Assert.Equal(new[] { "Step1", "Step2" }, executionOrder); + Assert.True(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_ShouldHandleStepExceptions() + { + // Arrange + var step1 = new Mock(); + var step2 = new Mock(); + + step1.Setup(x => x.ExecuteAsync(It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Step1 failed")); + + step2.Setup(x => x.ExecuteAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var steps = new List { step1.Object, step2.Object }; + var pipeline = new RadiusPipeline(steps); + var context = new RadiusPipelineContext( + CreateRequestPacket(), + new ClientConfiguration()); + + // Act & Assert + await Assert.ThrowsAsync( + () => pipeline.ExecuteAsync(context)); + + // Step2 не должен был выполниться после исключения + step2.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Never); + } + + [Fact] + public void Constructor_ShouldThrowWhenStepsIsNull() + { + // Act & Assert + Assert.Throws(() => new RadiusPipeline(null)); + } + + [Fact] + public async Task ExecuteAsync_ShouldThrowWhenContextIsNull() + { + // Arrange + var pipeline = new RadiusPipeline(new List()); + + // Act & Assert + await Assert.ThrowsAsync( + () => pipeline.ExecuteAsync(null)); + } + + private List CreateMockSteps(int count, List executionOrder) + { + var steps = new List(); + + for (int i = 1; i <= count; i++) + { + steps.Add(CreateMockStep($"Step{i}", executionOrder, terminate: false)); + } + + return steps; + } + + private IRadiusPipelineStep CreateMockStep(string name, List executionOrder, bool terminate) + { + var mock = new Mock(); + mock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(ctx => + { + executionOrder.Add(name); + if (terminate) + { + ctx.Terminate(); + } + }) + .Returns(Task.CompletedTask); + + return mock.Object; + } + + private RadiusPacket CreateRequestPacket() + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Any, 1812) + }; + packet.AddAttributeValue("User-Name", "testuser"); + return packet; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs new file mode 100644 index 00000000..3326d0c8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs @@ -0,0 +1,266 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class AccessChallengeStepTests + { + private readonly Mock _challengeProcessorProviderMock; + private readonly Mock> _loggerMock; + private readonly AccessChallengeStep _accessChallengeStep; + + public AccessChallengeStepTests() + { + _challengeProcessorProviderMock = new Mock(); + _loggerMock = new Mock>(); + + _accessChallengeStep = new AccessChallengeStep( + _challengeProcessorProviderMock.Object, + _loggerMock.Object + ); + } + + [Fact] + public async Task ExecuteAsync_WhenStateIsNullOrEmpty_ShouldReturnImmediately() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.RemoveAttribute("State"); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _challengeProcessorProviderMock.Verify( + x => x.GetChallengeProcessorByIdentifier(It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenProcessorNotFound_ShouldReturnImmediately() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state-123"); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns((IChallengeProcessor?)null); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _challengeProcessorProviderMock.Verify( + x => x.GetChallengeProcessorByIdentifier(It.Is( + id => id.RequestId == "test-state-123" && + id.ToString() == "test-client-test-state-123")), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenProcessorReturnsAccept_ShouldNotTerminate() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state-123"); + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(ChallengeStatus.Accept); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + processorMock.Verify( + x => x.ProcessChallengeAsync( + It.Is(id => id.RequestId == "test-state-123"), + context), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenProcessorReturnsReject_ShouldTerminate() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state-123"); + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(ChallengeStatus.Reject); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + processorMock.Verify( + x => x.ProcessChallengeAsync( + It.Is(id => id.RequestId == "test-state-123"), + context), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenProcessorReturnsInProcess_ShouldTerminate() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state-123"); + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(ChallengeStatus.InProcess); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + processorMock.Verify( + x => x.ProcessChallengeAsync( + It.Is(id => id.RequestId == "test-state-123"), + context), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenProcessorReturnsUnexpectedStatus_ShouldThrow() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state-123"); + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync((ChallengeStatus)999); // Invalid enum value + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => + _accessChallengeStep.ExecuteAsync(context)); + } + + [Fact] + public async Task ExecuteAsync_ShouldCreateCorrectChallengeIdentifier() + { + // Arrange + var context = CreateTestContext(); + context.ClientConfiguration.Name = "my-client"; + context.RequestPacket.ReplaceAttribute("State", "test-state-456"); + + var capturedIdentifier = (ChallengeIdentifier?)null; + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .Callback((id, ctx) => + { + capturedIdentifier = id; + }) + .ReturnsAsync(ChallengeStatus.Accept); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.NotNull(capturedIdentifier); + Assert.Equal("my-client", capturedIdentifier.ToString().Split('-')[0]); + Assert.Equal("challenge-state-456", capturedIdentifier.RequestId); + Assert.Equal("my-client-challenge-state-456", capturedIdentifier.ToString()); + } + + [Fact] + public async Task ExecuteAsync_ShouldLogDebugMessage() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.ReplaceAttribute("State", "test-state"); + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessChallengeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(ChallengeStatus.Accept); + + _challengeProcessorProviderMock + .Setup(x => x.GetChallengeProcessorByIdentifier(It.IsAny())) + .Returns(processorMock.Object); + + var logMessages = new List(); + _loggerMock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (level, eventId, state, exception, formatter) => + { + logMessages.Add(formatter(state, exception)); + }); + + // Act + await _accessChallengeStep.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => msg.Contains(nameof(AccessChallengeStep))); + } + + private static RadiusPipelineContext CreateTestContext() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret" + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + + requestPacket.AddAttributeValue("State", "initial-state"); + + return new RadiusPipelineContext(requestPacket, clientConfig); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessGroupsCheckingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessGroupsCheckingStepTests.cs new file mode 100644 index 00000000..e4751abf --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessGroupsCheckingStepTests.cs @@ -0,0 +1,222 @@ +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Core.Ldap.Name; +// using Multifactor.Core.Ldap.Schema; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +// using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +// { +// public class AccessGroupsCheckingStepTests +// { +// private readonly Mock _ldapAdapterMock; +// private readonly AccessGroupsCheckingStep _step; +// +// public AccessGroupsCheckingStepTests() +// { +// _ldapAdapterMock = new Mock(); +// var loggerMock = new Mock>(); +// _step = new AccessGroupsCheckingStep(_ldapAdapterMock.Object, loggerMock.Object); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldThrowArgumentNullException_WhenContextIsNull() +// { +// // Act & Assert +// await Assert.ThrowsAsync(() => _step.ExecuteAsync(null)); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldThrowArgumentNullException_WhenLdapConfigurationIsNull() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), It.IsAny()); +// +// // Act & Assert +// await Assert.ThrowsAsync(() => _step.ExecuteAsync(context)); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldThrowArgumentNullException_WhenLdapSchemaIsNull() +// { +// // Arrange +// var context = new RadiusPipelineContext(It.IsAny(), +// It.IsAny(), +// It.IsAny()) +// { +// LdapSchema = null +// }; +// +// // Act & Assert +// await Assert.ThrowsAsync(() => _step.ExecuteAsync(context)); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldSkip_WhenNoAccessGroups() +// { +// // Arrange +// var ldapConf = new LdapServerConfiguration() +// { +// AccessGroups = [] +// }; +// var context = new RadiusPipelineContext(It.IsAny(), +// It.IsAny(), +// ldapConf) +// { +// LdapSchema = It.IsAny(), +// LdapProfile = It.IsAny() +// }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.False(context.IsTerminated); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldSkip_WhenUnsupportedAccountType() +// { +// // Arrange +// var requestPacket = new RadiusPacket(It.IsAny()) +// { +// UserName = "testuser", +// AccountType = "local" +// }; +// var ldapConf = new LdapServerConfiguration() +// { +// AccessGroups = new List { new ("group1") } +// }; +// var context = new RadiusPipelineContext(requestPacket, It.IsAny(), +// ldapConf) +// { +// LdapSchema = It.IsAny(), +// LdapProfile = new LdapProfile(), +// IsDomainAccount = false, +// }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.False(context.IsTerminated); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldThrowArgumentNullException_WhenLdapProfileIsNull() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = new LdapServerConfiguration +// { +// AccessGroups = new List { "group1" } +// }, +// LdapSchema = It.IsAny(), +// LdapProfile = null, +// IsDomainAccount = true +// }; +// +// // Act & Assert +// await Assert.ThrowsAsync(() => _step.ExecuteAsync(context)); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldContinue_WhenUserIsMemberOfAccessGroupViaProfile() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = new LdapServerConfiguration +// { +// AccessGroups = new List { "group1", "group2" } +// }, +// LdapSchema = It.IsAny(), +// LdapProfile = new LdapProfile +// { +// Dn = "cn=user,dc=test", +// MemberOf = new List { "group2", "group3" } +// }, +// IsDomainAccount = true +// }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.False(context.IsTerminated); +// Assert.NotEqual(AuthenticationStatus.Reject, context.FirstFactorStatus); +// Assert.NotEqual(AuthenticationStatus.Reject, context.SecondFactorStatus); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldContinue_WhenLdapAdapterConfirmsMembership() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = new LdapServerConfiguration +// { +// AccessGroups = new List { "group1" }, +// ConnectionString = "ldap://test" +// }, +// LdapSchema = It.IsAny(), +// LdapProfile = new LdapProfile +// { +// Dn = "cn=user,dc=test", +// MemberOf = new List { "group3" } +// }, +// IsDomainAccount = true +// }; +// +// _ldapAdapterMock +// .Setup(x => x.IsMemberOf(It.IsAny())) +// .Returns(true); +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.False(context.IsTerminated); +// _ldapAdapterMock.Verify(x => x.IsMemberOf(It.IsAny()), Times.Once); +// } +// +// [Fact] +// public async Task ExecuteAsync_ShouldTerminate_WhenUserIsNotMemberOfAnyAccessGroup() +// { +// // Arrange +// var context = new RadiusPipelineContext +// { +// LdapConfiguration = new LdapServerConfiguration() +// { +// AccessGroups = new List { "group1" }, +// ConnectionString = "ldap://test" +// }, +// LdapProfile = new LdapProfile +// { +// Dn = "cn=user,dc=test", +// MemberOf = new List { "group2", "group3" } +// }, +// IsDomainAccount = true +// }; +// +// _ldapAdapterMock +// .Setup(x => x.IsMemberOf(It.IsAny())) +// .Returns(false); +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.True(context.IsTerminated); +// Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); +// Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); +// _ldapAdapterMock.Verify(x => x.IsMemberOf(It.IsAny()), Times.Once); +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs new file mode 100644 index 00000000..bf2094e3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs @@ -0,0 +1,125 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class AccessRequestFilteringStepTests + { + private readonly Mock> _loggerMock; + private readonly AccessRequestFilteringStep _step; + + public AccessRequestFilteringStepTests() + { + _loggerMock = new Mock>(); + _step = new AccessRequestFilteringStep(_loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_ShouldAllowAccessRequest() + { + // Arrange + var requestPacket = CreateRadiusPacket(PacketCode.AccessRequest); + var context = CreateContext(requestPacket); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + Assert.False(context.ShouldSkipResponse); + } + + [Theory] + [InlineData(PacketCode.AccountingRequest)] + [InlineData(PacketCode.AccountingResponse)] + [InlineData(PacketCode.StatusClient)] + [InlineData(PacketCode.DisconnectRequest)] + [InlineData(PacketCode.DisconnectAck)] + [InlineData(PacketCode.DisconnectNak)] + [InlineData(PacketCode.CoaRequest)] + [InlineData(PacketCode.CoaAck)] + [InlineData(PacketCode.CoaNak)] + public async Task ExecuteAsync_ShouldTerminateNonAccessRequests(PacketCode packetCode) + { + // Arrange + var requestPacket = CreateRadiusPacket(packetCode); + var context = CreateContext(requestPacket); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.True(context.ShouldSkipResponse); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Unprocessable packet type")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldLogClientInfoWhenTerminating() + { + // Arrange + var requestPacket = CreateRadiusPacket(PacketCode.AccountingRequest); + var context = CreateContext(requestPacket); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("192.168.1.100")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldHandleNullProxyEndpoint() + { + // Arrange + var requestPacket = CreateRadiusPacket(PacketCode.AccountingRequest); + requestPacket.ProxyEndpoint = null; + var context = CreateContext(requestPacket); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + // Не должно быть исключения + } + + private RadiusPacket CreateRadiusPacket(PacketCode code) + { + var header = new RadiusPacketHeader(code, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 1812) + }; + + return packet; + } + + private RadiusPipelineContext CreateContext(RadiusPacket requestPacket) + { + var clientConfig = new ClientConfiguration { Name = "TestClient" }; + return new RadiusPipelineContext(requestPacket, clientConfig); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs new file mode 100644 index 00000000..5b697bc1 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs @@ -0,0 +1,313 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class FirstFactorStepTests + { + private readonly Mock _processorProviderMock; + private readonly Mock _challengeProviderMock; + private readonly Mock> _loggerMock; + private readonly FirstFactorStep _step; + + public FirstFactorStepTests() + { + _processorProviderMock = new Mock(); + _challengeProviderMock = new Mock(); + _loggerMock = new Mock>(); + + _step = new FirstFactorStep( + _processorProviderMock.Object, + _challengeProviderMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenContextIsNull_ThrowsException() + { + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(null)); + } + + [Fact] + public async Task ExecuteAsync_WhenStatusNotAwaiting_DoesNothing() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Accept; // Not Awaiting + + // Act + await _step.ExecuteAsync(context); + + // Assert + _processorProviderMock.Verify( + x => x.GetProcessor(It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenStatusAwaiting_GetsAndRunsProcessor() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(AuthenticationSource.Radius)) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _processorProviderMock.Verify( + x => x.GetProcessor(AuthenticationSource.Radius), + Times.Once); + + processorMock.Verify( + x => x.ProcessFirstFactor(context), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenMustChangePasswordDomainEmpty_NoChallenge() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.MustChangePasswordDomain = ""; // Empty + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _challengeProviderMock.Verify( + x => x.GetChallengeProcessorByType(ChallengeType.PasswordChange), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenMustChangePasswordDomainSet_CreatesChallenge() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.MustChangePasswordDomain = "test-domain"; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + var challengeProcessorMock = new Mock(); + _challengeProviderMock + .Setup(x => x.GetChallengeProcessorByType(ChallengeType.PasswordChange)) + .Returns(challengeProcessorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _challengeProviderMock.Verify( + x => x.GetChallengeProcessorByType(ChallengeType.PasswordChange), + Times.Once); + + challengeProcessorMock.Verify( + x => x.AddChallengeContext(context), + Times.Once); + + Assert.Equal(AuthenticationStatus.Awaiting, context.FirstFactorStatus); + } + + [Fact] + public async Task ExecuteAsync_WhenChallengeProcessorNotFound_ThrowsException() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.MustChangePasswordDomain = "test-domain"; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + _challengeProviderMock + .Setup(x => x.GetChallengeProcessorByType(ChallengeType.PasswordChange)) + .Returns((IChallengeProcessor?)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _step.ExecuteAsync(context)); + + Assert.Contains("Challenge processor", exception.Message); + } + + [Fact] + public async Task ExecuteAsync_WhenFirstFactorReject_Terminates() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessFirstFactor(context)) + .Callback(() => context.FirstFactorStatus = AuthenticationStatus.Reject); + + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_WhenFirstFactorAccept_DoesNotTerminate() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + processorMock.Setup(x => x.ProcessFirstFactor(context)) + .Callback(() => context.FirstFactorStatus = AuthenticationStatus.Accept); + + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_LogsDebugMessage() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(It.IsAny())) + .Returns(processorMock.Object); + + var logMessages = new List(); + _loggerMock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (level, eventId, state, exception, formatter) => + { + if (level == LogLevel.Debug) + logMessages.Add(formatter(state, exception)); + }); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains(nameof(FirstFactorStep)) && + msg.Contains("started")); + } + + [Fact] + public async Task ExecuteAsync_WithLdapSource_GetsCorrectProcessor() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(AuthenticationSource.Ldap); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(AuthenticationSource.Ldap)) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _processorProviderMock.Verify( + x => x.GetProcessor(AuthenticationSource.Ldap), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WithNoneSource_GetsCorrectProcessor() + { + // Arrange + var clientConfiguration = CreateTestClientConfiguration(AuthenticationSource.None); + var context = CreateTestContext(clientConfiguration); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + + var processorMock = new Mock(); + _processorProviderMock + .Setup(x => x.GetProcessor(AuthenticationSource.None)) + .Returns(processorMock.Object); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _processorProviderMock.Verify( + x => x.GetProcessor(AuthenticationSource.None), + Times.Once); + } + + private static IClientConfiguration CreateTestClientConfiguration(AuthenticationSource source = AuthenticationSource.Radius) + { + return new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret", + FirstFactorAuthenticationSource = AuthenticationSource.Radius + }; + } + + private static RadiusPipelineContext CreateTestContext(ClientConfiguration clientConfiguration) + { + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + + return new RadiusPipelineContext(requestPacket, clientConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs new file mode 100644 index 00000000..3bd2bbe5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs @@ -0,0 +1,151 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class IpWhiteListStepTests + { + private readonly Mock> _loggerMock; + private readonly IpWhiteListStep _step; + + public IpWhiteListStepTests() + { + _loggerMock = new Mock>(); + _step = new IpWhiteListStep(_loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_ShouldAllowWhenIpInWhiteList() + { + // Arrange + var whiteList = new List + { + IPAddressRange.Parse("192.168.1.0/24"), + IPAddressRange.Parse("10.0.0.0/8") + }; + + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + IpWhiteList = whiteList + }; + + var requestPacket = CreateAccessRequestPacket("192.168.1.100"); + var context = new RadiusPipelineContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("is in the allowed IP range")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldTerminateWhenIpNotInWhiteList() + { + // Arrange + var whiteList = new List + { + IPAddressRange.Parse("192.168.1.0/24") + }; + + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + IpWhiteList = whiteList + }; + + var requestPacket = CreateAccessRequestPacket("10.0.0.100"); // Not in whitelist + var context = new RadiusPipelineContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("is not in the allowed IP range")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldSkipWhenWhiteListEmpty() + { + // Arrange + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + IpWhiteList = new List() + }; + + var requestPacket = CreateAccessRequestPacket("192.168.1.100"); + var context = new RadiusPipelineContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_ShouldUseCallingStationIdWhenAvailable() + { + // Arrange + var whiteList = new List + { + IPAddressRange.Parse("192.168.1.26-192.168.1.32"), + }; + + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + IpWhiteList = whiteList + }; + + var requestPacket = CreateAccessRequestPacket("192.168.1.100"); + requestPacket.AddAttributeValue("Calling-Station-Id", "192.168.1.50"); // Different IP + var context = new RadiusPipelineContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); // Should reject because 192.168.1.50 is not in whitelist + } + + private RadiusPacket CreateAccessRequestPacket(string remoteIp) + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Parse(remoteIp), 1812) + }; + packet.AddAttributeValue("User-Name", "testuser"); + return packet; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs new file mode 100644 index 00000000..d68520d5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs @@ -0,0 +1,285 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class LdapSchemaLoadingStepTests + { + private readonly Mock _ldapAdapterMock; + private readonly Mock _cacheMock; + private readonly Mock> _loggerMock; + private readonly LdapSchemaLoadingStep _step; + + public LdapSchemaLoadingStepTests() + { + _ldapAdapterMock = new Mock(); + _cacheMock = new Mock(); + _loggerMock = new Mock>(); + + _step = new LdapSchemaLoadingStep( + _ldapAdapterMock.Object, + _cacheMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenContextIsNull_ThrowsException() + { + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(null)); + } + + [Fact] + public async Task ExecuteAsync_WhenLdapConfigurationIsNull_ThrowsException() + { + // Arrange + var context = CreateTestContextWithoutLdap(); + + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(context)); + } + + [Fact] + public async Task ExecuteAsync_WhenSchemaInCache_UsesCachedSchema() + { + // Arrange + var context = CreateTestContext(); + var cachedSchema = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("ldap://test.com", out cachedSchema)) + .Returns(true); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Same(cachedSchema, context.LdapSchema); + _ldapAdapterMock.Verify( + x => x.LoadSchema(It.IsAny()), + Times.Never); + + VerifyDebugLog("from cache"); + } + + [Fact] + public async Task ExecuteAsync_WhenSchemaNotInCache_LoadsFromLdap() + { + // Arrange + var context = CreateTestContext(); + var loadedSchema = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("ldap://test.com", out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.LoadSchema(It.Is(d => + d.ConnectionString == "ldap://test.com" && + d.UserName == "admin" && + d.Password == "password" && + d.BindTimeoutInSeconds == 30))) + .Returns(loadedSchema); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Same(loadedSchema, context.LdapSchema); + _cacheMock.Verify( + x => x.Set( + "ldap://test.com", + loadedSchema, + It.Is(d => d > DateTimeOffset.Now)), + Times.Once); + + VerifyDebugLog("saved in cache"); + } + + [Fact] + public async Task ExecuteAsync_WhenSchemaLoadFails_ThrowsException() + { + // Arrange + var context = CreateTestContext(); + + _cacheMock + .Setup(x => x.TryGetValue("ldap://test.com", out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.LoadSchema(It.IsAny())) + .Returns((ILdapSchema?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(context)); + + VerifyWarningLog(); + } + + [Fact] + public async Task ExecuteAsync_WhenSchemaLoaded_SetsCacheForOneHour() + { + // Arrange + var context = CreateTestContext(); + var loadedSchema = new Mock().Object; + DateTimeOffset? cachedExpiration = null; + + _cacheMock + .Setup(x => x.TryGetValue("ldap://test.com", out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.LoadSchema(It.IsAny())) + .Returns(loadedSchema); + + _cacheMock + .Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((key, value, expiration) => + { + cachedExpiration = expiration; + }); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.NotNull(cachedExpiration); + var expectedExpiration = DateTimeOffset.Now.AddHours(1); + Assert.True(cachedExpiration.Value > DateTimeOffset.Now && + cachedExpiration.Value <= expectedExpiration); + } + + [Fact] + public async Task ExecuteAsync_LogsDebugOnStart() + { + // Arrange + var context = CreateTestContext(); + var cachedSchema = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("ldap://test.com", out cachedSchema)) + .Returns(true); + + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains(nameof(LdapSchemaLoadingStep)) && + msg.Contains("started")); + } + + [Fact] + public async Task ExecuteAsync_WithDifferentConnectionString_UsesItAsCacheKey() + { + // Arrange + var context = CreateTestContext(); + // context.LdapConfiguration.ConnectionString = "ldap://another-server:389"; + var cachedSchema = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("ldap://another-server:389", out cachedSchema)) + .Returns(true); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _cacheMock.Verify( + x => x.TryGetValue("ldap://another-server:389", out cachedSchema), + Times.Once); + } + + private void SetupLogCapture(List logMessages) + { + _loggerMock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (level, eventId, state, exception, formatter) => + { + logMessages.Add(formatter(state, exception)); + }); + } + + private void VerifyDebugLog(string expectedMessage) + { + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains(expectedMessage)), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + private void VerifyWarningLog() + { + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Unable to load")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + private static RadiusPipelineContext CreateTestContext() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret" + }; + + var ldapConfig = new LdapServerConfiguration + { + ConnectionString = "ldap://test.com", + Username = "admin", + Password = "password", + BindTimeoutSeconds = 30 + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig); + return context; + } + + private static RadiusPipelineContext CreateTestContextWithoutLdap() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret" + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + + var context = new RadiusPipelineContext(requestPacket, clientConfig); + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthCheckStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthCheckStepTests.cs new file mode 100644 index 00000000..b4b1262e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthCheckStepTests.cs @@ -0,0 +1,168 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class PreAuthCheckStepTests + { + private readonly Mock> _loggerMock; + private readonly Mock _ldapAdapterMock; + private readonly PreAuthCheckStep _step; + + public PreAuthCheckStepTests() + { + _loggerMock = new Mock>(); + _ldapAdapterMock = new Mock(); + + _step = new PreAuthCheckStep(_loggerMock.Object, _ldapAdapterMock.Object); + } + + [Fact] + public async Task ExecuteAsync_ShouldTerminateWhenOtpRequiredButMissing() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser", "password"); // Нет OTP + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = PreAuthMode.Otp + }; + var context = CreateContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Reject, context.SecondFactorStatus); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("otp code is empty")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldContinueWhenOtpRequiredAndPresent() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser", "password1234567890"); // С OTP + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = PreAuthMode.Otp + }; + var context = CreateContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Pre-auth check")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldContinueWhenPreAuthModeIsNone() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser", "password"); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = PreAuthMode.None + }; + var context = CreateContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_ShouldContinueWhenPreAuthModeIsAny() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser", "password"); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = PreAuthMode.Any + }; + var context = CreateContext(requestPacket, clientConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_ShouldThrowOnUnknownPreAuthMethod() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser", "password"); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = (PreAuthMode)999 // Неизвестный метод + }; + var context = CreateContext(requestPacket, clientConfig); + + // Act & Assert + await Assert.ThrowsAsync( + () => _step.ExecuteAsync(context)); + } + + private RadiusPacket CreateRadiusPacket(string userName, string password) + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Any, 228) + }; + + packet.AddAttributeValue("User-Name", userName); + packet.AddAttributeValue("User-Password", password); + + return packet; + } + + private RadiusPipelineContext CreateContext(RadiusPacket requestPacket, IClientConfiguration clientConfig) + { + var context = new RadiusPipelineContext(requestPacket, clientConfig); + + var passphrase = UserPassphrase.Parse( + requestPacket.TryGetUserPassword(), + clientConfig.PreAuthenticationMethod ?? PreAuthMode.None); + + context.Passphrase = passphrase; + + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs new file mode 100644 index 00000000..32e45cfe --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs @@ -0,0 +1,187 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class PreAuthPostCheckTests + { + private readonly Mock> _loggerMock; + private readonly PreAuthPostCheck _step; + + public PreAuthPostCheckTests() + { + _loggerMock = new Mock>(); + _step = new PreAuthPostCheck(_loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenSecondFactorAccept_DoesNotTerminate() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Accept; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_WhenSecondFactorBypass_DoesNotTerminate() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Bypass; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_WhenSecondFactorReject_Terminates() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Reject; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_WhenSecondFactorAwaiting_Terminates() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_WhenUserNameNull_LogsWithNull() + { + // Arrange + var context = CreateTestContext(); + context.RequestPacket.RemoveAttribute("User-Name"); + context.SecondFactorStatus = AuthenticationStatus.Accept; + + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains("continued pipeline") && + msg.Contains("''")); + } + + [Fact] + public async Task ExecuteAsync_WhenLdapSchemaNull_LogsWithoutDomain() + { + // Arrange + var context = CreateTestContext(); + context.LdapSchema = null; + context.SecondFactorStatus = AuthenticationStatus.Reject; + + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains("terminated pipeline") && + !msg.Contains("at '")); + } + + [Fact] + public async Task ExecuteAsync_LogsDebugOnStart() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Accept; + + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains(nameof(PreAuthPostCheck)) && + msg.Contains("started")); + } + + [Fact] + public async Task ExecuteAsync_WhenAlreadyTerminated_StillLogs() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Reject; + context.Terminate(); // Already terminated + + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => msg.Contains("terminated pipeline")); + } + + private void SetupLogCapture(List logMessages) + { + _loggerMock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (level, eventId, state, exception, formatter) => + { + if (level == LogLevel.Debug) + logMessages.Add(formatter(state, exception)); + }); + } + + private static RadiusPipelineContext CreateTestContext() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret" + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + requestPacket.AddAttributeValue("User-Name", "testuser"); + + return new RadiusPipelineContext(requestPacket, clientConfig); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs new file mode 100644 index 00000000..88d73e73 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs @@ -0,0 +1,255 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class ProfileLoadingStepTests + { + private readonly Mock _ldapAdapterMock; + private readonly Mock _cacheMock; + private readonly Mock> _loggerMock; + private readonly ProfileLoadingStep _step; + + public ProfileLoadingStepTests() + { + _ldapAdapterMock = new Mock(); + _cacheMock = new Mock(); + _loggerMock = new Mock>(); + + _step = new ProfileLoadingStep( + _ldapAdapterMock.Object, + _cacheMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenLocalAccount_SkipsStep() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + context.RequestPacket.AddAttributeValue("Acct-Authentic", new[]{2}); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Null(context.LdapProfile); + _cacheMock.Verify(x => x.TryGetValue(It.IsAny(), out It.Ref.IsAny), Times.Never); + _ldapAdapterMock.Verify(x => x.FindUserProfile(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenUserNameEmpty_SkipsStep() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + context.RequestPacket.ReplaceAttribute("User-Name", ""); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Null(context.LdapProfile); + } + + [Fact] + public async Task ExecuteAsync_WhenLdapSchemaNull_SkipsStep() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + context.LdapSchema = null; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Null(context.LdapProfile); + } + + [Fact] + public async Task ExecuteAsync_WhenProfileInCache_UsesCachedProfile() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + var cachedProfile = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("testuser-DC=test,DC=com", out cachedProfile)) + .Returns(true); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Same(cachedProfile, context.LdapProfile); + _ldapAdapterMock.Verify(x => x.FindUserProfile(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenProfileNotInCache_LoadsFromLdap() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + var loadedProfile = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("testuser-DC=test,DC=com", out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.FindUserProfile(It.IsAny())) + .Returns(loadedProfile); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Same(loadedProfile, context.LdapProfile); + _cacheMock.Verify( + x => x.Set("testuser-DC=test,DC=com", loadedProfile, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenProfileNotFound_ThrowsException() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + + _cacheMock + .Setup(x => x.TryGetValue("testuser-DC=test,DC=com", out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.FindUserProfile(It.IsAny())) + .Returns((ILdapProfile?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(context)); + } + + [Fact] + public async Task ExecuteAsync_IncludesCorrectAttributes() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + + // Add some reply attributes + var replyAttribute = new RadiusReplyAttribute { Name = "department" }; + context.ClientConfiguration.ReplyAttributes = new Dictionary + { + ["TestAttribute"] = [replyAttribute] + }; + + var loadedProfile = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue(It.IsAny(), out It.Ref.IsAny)) + .Returns(false); + + _ldapAdapterMock + .Setup(x => x.FindUserProfile(It.Is(r => + r.AttributeNames != null && + r.AttributeNames.Any(a => a.Value == "memberOf") && + r.AttributeNames.Any(a => a.Value == "userPrincipalName") && + r.AttributeNames.Any(a => a.Value == "phone") && + r.AttributeNames.Any(a => a.Value == "mail") && + r.AttributeNames.Any(a => a.Value == "displayName") && + r.AttributeNames.Any(a => a.Value == "email") && + r.AttributeNames.Any(a => a.Value == "customId") && + r.AttributeNames.Any(a => a.Value == "department") && + r.AttributeNames.Any(a => a.Value == "mobile") && + r.AttributeNames.Any(a => a.Value == "homePhone")))) + .Returns(loadedProfile); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _ldapAdapterMock.Verify( + x => x.FindUserProfile(It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WithDifferentUserName_CreatesCorrectCacheKey() + { + // Arrange + var ldapConfig = CreateTestLdapServerConfiguration(); + var context = CreateTestContext(ldapConfig); + context.RequestPacket.ReplaceAttribute("User-Name", "another.user@domain.com"); + + var cachedProfile = new Mock().Object; + + _cacheMock + .Setup(x => x.TryGetValue("another.user@domain.com-DC=test,DC=com", out cachedProfile)) + .Returns(true); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Same(cachedProfile, context.LdapProfile); + } + + private static LdapServerConfiguration CreateTestLdapServerConfiguration(string identityAttribute = "", string[]? phoneAttributes = null) + { + return new LdapServerConfiguration + { + ConnectionString = "ldap://test.com", + Username = "admin", + Password = "password", + BindTimeoutSeconds = 30, + IdentityAttribute = identityAttribute, + PhoneAttributes = phoneAttributes ?? [] + }; + } + private static RadiusPipelineContext CreateTestContext(LdapServerConfiguration? ldapServerConfiguration = null) + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret", + ReplyAttributes = new Dictionary>() + }; + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Any, 228), + }; + requestPacket.AddAttributeValue("User-Name", "testuser"); + + var ldapSchemaMock = new Mock(); + ldapSchemaMock.Setup(x => x.NamingContext).Returns(new DistinguishedName("DC=test,DC=com")); + var ldapProfileMock = new Mock(); + + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapServerConfiguration) + { + LdapSchema = ldapSchemaMock.Object, + LdapProfile = ldapProfileMock.Object + }; + + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs new file mode 100644 index 00000000..68fffd18 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs @@ -0,0 +1,328 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class SecondFactorStepTests + { + private readonly Mock _apiServiceMock; + private readonly Mock _challengeProviderMock; + private readonly Mock _ldapAdapterMock; + private readonly Mock> _loggerMock; + private readonly SecondFactorStep _step; + + public SecondFactorStepTests() + { + _apiServiceMock = new Mock( + Mock.Of(), + Mock.Of(), + Mock.Of>()); + + _challengeProviderMock = new Mock(); + _ldapAdapterMock = new Mock(); + _loggerMock = new Mock>(); + + _step = new SecondFactorStep( + _apiServiceMock.Object, + _challengeProviderMock.Object, + _ldapAdapterMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenContextNull_ThrowsException() + { + // Act & Assert + await Assert.ThrowsAsync(() => + _step.ExecuteAsync(null)); + } + + [Fact] + public async Task ExecuteAsync_WhenSecondFactorNotAwaiting_SetsBypass() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Accept; // Not Awaiting + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Bypass, context.SecondFactorStatus); + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenVendorAclRequestAndRadiusSource_Bypasses() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + context.RequestPacket.AddAttributeValue("User-Name", "#ACSACL#-IP");// Vendor ACL + context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.Radius; + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Bypass, context.SecondFactorStatus); + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenVendorAclRequestAndLdapSource_CallsApi() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + context.RequestPacket.AddAttributeValue("User-Name", "#ACSACL#-IP");// Vendor ACL + context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.Ldap; + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept)); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(context, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenUserInSecondFaBypassGroup_Bypasses() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + _ldapAdapterMock + .Setup(x => x.IsMemberOf(It.IsAny())) + .Returns(true); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Bypass, context.SecondFactorStatus); + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenUserInSecondFaGroup_CallsApi() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + _ldapAdapterMock + .Setup(x => x.IsMemberOf(It.IsAny())) + .Returns(true); + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept)); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(context, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenUserNotInSecondFaGroup_Bypasses() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + _ldapAdapterMock + .Setup(x => x.IsMemberOf(It.IsAny())) + .Returns(false); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Bypass, context.SecondFactorStatus); + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task ExecuteAsync_WhenLocalAccount_CallsApi() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + context.RequestPacket.AddAttributeValue("Acct-Authentic", new[]{2}); //local + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept)); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(context, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenApiReturnsAccept_SetsStatus() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept, "state123", "Welcome")); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Accept, context.SecondFactorStatus); + Assert.Equal("state123", context.ResponseInformation.State); + Assert.Equal("Welcome", context.ResponseInformation.ReplyMessage); + } + + [Fact] + public async Task ExecuteAsync_WhenApiReturnsAwaiting_CreatesChallenge() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + + var challengeProcessorMock = new Mock(); + _challengeProviderMock + .Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)) + .Returns(challengeProcessorMock.Object); + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, It.IsAny())) + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Awaiting, "challenge-state", "Enter code")); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Equal(AuthenticationStatus.Awaiting, context.SecondFactorStatus); + challengeProcessorMock.Verify( + x => x.AddChallengeContext(context), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenNoLdapConfiguration_CallsApi() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + // context.LdapConfiguration = null; + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, true)) // Should be true when no LDAP config + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept)); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(context, true), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhenLocalAccount_ShouldCacheReturnsFalse() + { + // Arrange + var context = CreateTestContext(); + context.SecondFactorStatus = AuthenticationStatus.Awaiting; + context.RequestPacket.AddAttributeValue("Acct-Authentic", new[]{2}); // local + + _apiServiceMock + .Setup(x => x.CreateSecondFactorRequestAsync(context, false)) // Should be false for local account + .ReturnsAsync(new SecondFactorResponse(AuthenticationStatus.Accept)); + + // Act + await _step.ExecuteAsync(context); + + // Assert + _apiServiceMock.Verify( + x => x.CreateSecondFactorRequestAsync(context, false), + Times.Once); + } + + private static RadiusPipelineContext CreateTestContext() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret", + FirstFactorAuthenticationSource = AuthenticationSource.Ldap + }; + + var ldapConfig = new LdapServerConfiguration + { + ConnectionString = "ldap://test.com", + Username = "admin", + Password = "password", + BindTimeoutSeconds = 30, + SecondFaGroups = new List + { + new("CN=2FAGroup,DC=test,DC=com") + }, + AuthenticationCacheGroups = new List() + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])); + requestPacket.AddAttributeValue("User-Name", "testuser");// Vendor ACL + requestPacket.AddAttributeValue("Acct-Authentic", new[]{1});// domain + + var ldapProfileMock = new Mock(); + ldapProfileMock.Setup(x => x.MemberOf).Returns(new List()); + + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig) + { + LdapProfile = ldapProfileMock.Object, + SecondFactorStatus = AuthenticationStatus.Awaiting + }; + + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/StatusServerFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/StatusServerFilteringStepTests.cs new file mode 100644 index 00000000..777f15cb --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/StatusServerFilteringStepTests.cs @@ -0,0 +1,199 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class StatusServerFilteringStepTests + { + private readonly ApplicationVariables _appVars; + private readonly Mock> _loggerMock; + private readonly StatusServerFilteringStep _step; + + public StatusServerFilteringStepTests() + { + _appVars = new ApplicationVariables + { + AppVersion = "1.2.3", + StartedAt = DateTime.Now.AddDays(-1).AddHours(-2).AddMinutes(-30) // 1 day, 2 hours, 30 minutes ago + }; + + _loggerMock = new Mock>(); + _step = new StatusServerFilteringStep(_appVars, _loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenStatusServer_SetsResponseAndTerminates() + { + // Arrange + var context = CreateTestContext(PacketCode.StatusServer); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Accept, context.FirstFactorStatus); + Assert.Equal(AuthenticationStatus.Accept, context.SecondFactorStatus); + + Assert.Contains("Server up", context.ResponseInformation.ReplyMessage); + Assert.Contains("1 days", context.ResponseInformation.ReplyMessage); + Assert.Contains("02:30", context.ResponseInformation.ReplyMessage); // Hours and minutes + Assert.Contains("ver.: 1.2.3", context.ResponseInformation.ReplyMessage); + } + + [Fact] + public async Task ExecuteAsync_WhenNotStatusServer_DoesNothing() + { + // Arrange + var context = CreateTestContext(PacketCode.AccessRequest); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Awaiting, context.FirstFactorStatus); + Assert.Equal(AuthenticationStatus.Awaiting, context.SecondFactorStatus); + Assert.Null(context.ResponseInformation.ReplyMessage); + } + + [Theory] + [InlineData(PacketCode.AccessRequest)] + [InlineData(PacketCode.AccessAccept)] + [InlineData(PacketCode.AccessReject)] + [InlineData(PacketCode.AccessChallenge)] + [InlineData(PacketCode.AccountingRequest)] + [InlineData(PacketCode.AccountingResponse)] + [InlineData(PacketCode.StatusClient)] + [InlineData(PacketCode.DisconnectRequest)] + [InlineData(PacketCode.DisconnectAck)] + [InlineData(PacketCode.DisconnectNak)] + [InlineData(PacketCode.CoaRequest)] + [InlineData(PacketCode.CoaAck)] + [InlineData(PacketCode.CoaNak)] + public async Task ExecuteAsync_ForNonStatusServerPackets_DoesNotTerminate(PacketCode packetCode) + { + // Arrange + var context = CreateTestContext(packetCode); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Awaiting, context.FirstFactorStatus); + Assert.Equal(AuthenticationStatus.Awaiting, context.SecondFactorStatus); + } + + [Fact] + public async Task ExecuteAsync_WhenAppVersionNull_StillWorks() + { + // Arrange + var appVars = new ApplicationVariables + { + AppVersion = null, + StartedAt = DateTime.Now.AddHours(-5) + }; + + var step = new StatusServerFilteringStep(appVars, _loggerMock.Object); + var context = CreateTestContext(PacketCode.StatusServer); + + // Act + await step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.Contains("Server up", context.ResponseInformation.ReplyMessage); + Assert.Contains("ver.:", context.ResponseInformation.ReplyMessage); + } + + [Fact] + public async Task ExecuteAsync_WhenUptimeZeroDays_ShowsCorrectFormat() + { + // Arrange + var appVars = new ApplicationVariables + { + AppVersion = "1.0", + StartedAt = DateTime.Now.AddHours(-3).AddMinutes(-15) // 3 hours, 15 minutes ago + }; + + var step = new StatusServerFilteringStep(appVars, _loggerMock.Object); + var context = CreateTestContext(PacketCode.StatusServer); + + // Act + await step.ExecuteAsync(context); + + // Assert + Assert.Contains("0 days", context.ResponseInformation.ReplyMessage); + Assert.Contains("03:15", context.ResponseInformation.ReplyMessage); + } + + [Fact] + public async Task ExecuteAsync_LogsDebugOnStart() + { + // Arrange + var context = CreateTestContext(PacketCode.StatusServer); + var logMessages = new List(); + SetupLogCapture(logMessages); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.Contains(logMessages, msg => + msg.Contains(nameof(StatusServerFilteringStep)) && + msg.Contains("started")); + } + + [Fact] + public async Task ExecuteAsync_ReturnsCompletedTask() + { + // Arrange + var context = CreateTestContext(PacketCode.AccessRequest); + + // Act + var task = _step.ExecuteAsync(context); + + // Assert + Assert.True(task.IsCompleted); + await task; + } + + private void SetupLogCapture(List logMessages) + { + _loggerMock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (level, eventId, state, exception, formatter) => + { + if (level == LogLevel.Debug) + logMessages.Add(formatter(state, exception)); + }); + } + + private static RadiusPipelineContext CreateTestContext(PacketCode packetCode) + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret" + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(packetCode, 1, new byte[16])); + + return new RadiusPipelineContext(requestPacket, clientConfig); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserGroupLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserGroupLoadingStepTests.cs new file mode 100644 index 00000000..371a5767 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserGroupLoadingStepTests.cs @@ -0,0 +1,270 @@ +// using Microsoft.Extensions.Logging; +// using Moq; +// using Multifactor.Core.Ldap.Name; +// using Multifactor.Core.Ldap.Schema; +// using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +// using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +// using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +// using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; +// +// namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +// { +// public class UserGroupLoadingStepTests +// { +// private readonly Mock _ldapAdapterMock; +// private readonly UserGroupLoadingStep _step; +// +// public UserGroupLoadingStepTests() +// { +// _ldapAdapterMock = new Mock(); +// var loggerMock = new Mock>(); +// +// _step = new UserGroupLoadingStep( +// _ldapAdapterMock.Object, +// loggerMock.Object); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenRequestNotAccepted_Skips() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Reject; // Not accepted +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.Null(context.UserGroups); +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenGroupsNotRequired_Skips() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// // No reply attributes require groups +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.Null(context.UserGroups); +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenLocalAccount_Skips() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// context.RequestPacket.AddAttributeValue("Acct-Authentic", new[]{2}); //local +// +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.Null(context.UserGroups); +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenAccepted_LoadsGroupsFromProfile() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// +// // Add reply attribute that requires groups +// var replyAttribute = new RadiusReplyAttribute { IsMemberOf = true }; +// context.ClientConfiguration.ReplyAttributes = new Dictionary +// { +// ["Test"] = new[] { replyAttribute } +// }; +// +// // Setup memberOf groups +// var group1 = new DistinguishedName("CN=Group1,DC=test,DC=com"); +// var group2 = new DistinguishedName("CN=Group2,DC=test,DC=com"); +// context.LdapProfile.MemberOf = new List { group1, group2 }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.NotNull(context.UserGroups); +// Assert.Equal(2, context.UserGroups.Count); +// Assert.Contains("Group1", context.UserGroups); +// Assert.Contains("Group2", context.UserGroups); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenNestedGroupsDisabled_DoesNotLoadFromLdap() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// context.LdapConfiguration.LoadNestedGroups = false; +// +// var replyAttribute = new RadiusReplyAttribute { IsMemberOf = true }; +// context.ClientConfiguration.ReplyAttributes = new Dictionary +// { +// ["Test"] = new[] { replyAttribute } +// }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenNestedGroupsEnabledAndBaseDns_LoadsFromContainers() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// context.LdapConfiguration.LoadNestedGroups = true; +// context.LdapConfiguration.NestedGroupsBaseDns = new List +// { +// new("OU=Groups,DC=test,DC=com"), +// new("OU=Security,DC=test,DC=com") +// }; +// +// var replyAttribute = new Mock(); +// replyAttribute.Setup(x => x.IsMemberOf).Returns(true); +// +// context.ClientConfiguration.ReplyAttributes = new Dictionary +// { +// ["Test"] = [replyAttribute.Object] +// }; +// +// _ldapAdapterMock +// .SetupSequence(x => x.LoadUserGroups(It.IsAny())) +// .Returns(new List { "NestedGroup1", "NestedGroup2" }) +// .Returns(new List { "SecurityGroup1" }); +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Exactly(2)); +// +// Assert.NotNull(context.UserGroups); +// Assert.Contains("NestedGroup1", context.UserGroups); +// Assert.Contains("NestedGroup2", context.UserGroups); +// Assert.Contains("SecurityGroup1", context.UserGroups); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenNestedGroupsEnabledNoBaseDns_LoadsFromRoot() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// context.LdapConfiguration.LoadNestedGroups = true; +// context.LdapConfiguration.NestedGroupsBaseDns = new List(); // Empty +// +// var replyAttribute = new RadiusReplyAttribute { IsMemberOf = true }; +// context.ClientConfiguration.ReplyAttributes = new Dictionary +// { +// ["Test"] = new[] { replyAttribute } +// }; +// +// _ldapAdapterMock +// .Setup(x => x.LoadUserGroups(It.IsAny())) +// .Returns(new List { "RootGroup1", "RootGroup2" }); +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// _ldapAdapterMock.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Once); +// +// Assert.NotNull(context.UserGroups); +// Assert.Contains("RootGroup1", context.UserGroups); +// Assert.Contains("RootGroup2", context.UserGroups); +// } +// +// [Fact] +// public async Task ExecuteAsync_WhenUserGroupConditionInReplyAttributes_GroupsRequired() +// { +// // Arrange +// var context = CreateTestContext(); +// context.FirstFactorStatus = AuthenticationStatus.Accept; +// context.SecondFactorStatus = AuthenticationStatus.Accept; +// +// // Reply attribute with user group condition +// var replyAttribute = new RadiusReplyAttribute +// { +// UserGroupCondition = new List { "AdminGroup" } +// }; +// context.ClientConfiguration.ReplyAttributes = new Dictionary +// { +// ["Test"] = new[] { replyAttribute } +// }; +// +// // Act +// await _step.ExecuteAsync(context); +// +// // Assert +// Assert.NotNull(context.UserGroups); +// // Groups should be loaded even though IsMemberOf is false +// } +// +// private static RadiusPipelineContext CreateTestContext() +// { +// var clientConfig = new ClientConfiguration +// { +// Name = "test-client", +// RadiusSharedSecret = "test-secret", +// ReplyAttributes = new Dictionary() +// }; +// +// var ldapConfig = new LdapServerConfiguration +// { +// ConnectionString = "ldap://test.com", +// Username = "admin", +// Password = "password", +// BindTimeoutSeconds = 30, +// LoadNestedGroups = false, +// NestedGroupsBaseDns = new List() +// }; +// +// var requestPacket = new RadiusPacket( +// new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])) +// { +// UserName = "testuser", +// AccountType = AccountType.Domain +// }; +// +// var ldapSchemaMock = new Mock(); +// var ldapProfileMock = new Mock(); +// var userDnMock = new Mock(); +// userDnMock.Setup(x => x.StringRepresentation).Returns("CN=testuser,DC=test,DC=com"); +// ldapProfileMock.Setup(x => x.Dn).Returns(userDnMock.Object); +// ldapProfileMock.Setup(x => x.MemberOf).Returns(new List()); +// +// var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig); +// context.LdapSchema = ldapSchemaMock.Object; +// context.LdapProfile = ldapProfileMock.Object; +// +// return context; +// } +// } +// } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserNameValidationStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserNameValidationStepTests.cs new file mode 100644 index 00000000..34ef3f56 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserNameValidationStepTests.cs @@ -0,0 +1,190 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.Steps +{ + public class UserNameValidationStepTests + { + private readonly Mock> _loggerMock; + private readonly UserNameValidationStep _step; + + public UserNameValidationStepTests() + { + _loggerMock = new Mock>(); + _step = new UserNameValidationStep(_loggerMock.Object); + } + + [Fact] + public async Task ExecuteAsync_ShouldSkipWhenNoLdapConfiguration() + { + // Arrange + var requestPacket = CreateRadiusPacket("user@domain.com"); + var context = CreateContext(requestPacket, null); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No LDAP server configuration")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task ExecuteAsync_ShouldTerminateWhenRequiresUpnAndNotUpn() + { + // Arrange + var requestPacket = CreateRadiusPacket("DOMAIN\\user"); // NetBIOS, не UPN + var ldapConfig = new LdapServerConfiguration { RequiresUpn = true }; + var context = CreateContext(requestPacket, ldapConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.True(context.IsTerminated); + Assert.Equal(AuthenticationStatus.Reject, context.FirstFactorStatus); + Assert.Equal("User name in UPN format is required.", context.ResponseInformation.ReplyMessage); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("User name in UPN format is required")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Theory] + [InlineData("user@domain.com", true)] // Включенный суффикс + [InlineData("user@other.com", false)] // Исключенный суффикс + public async Task ExecuteAsync_ShouldValidateIncludedSuffixes(string userName, bool shouldPass) + { + // Arrange + var requestPacket = CreateRadiusPacket(userName); + var ldapConfig = new LdapServerConfiguration + { + IncludedSuffixes = new List { "domain.com", "example.com" } + }; + var context = CreateContext(requestPacket, ldapConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + if (shouldPass) + { + Assert.False(context.IsTerminated); + } + else + { + Assert.True(context.IsTerminated); + Assert.Equal("UPN suffix is not permitted.", context.ResponseInformation.ReplyMessage); + } + } + + [Theory] + [InlineData("user@domain.com", false)] // Исключенный суффикс + [InlineData("user@other.com", true)] // Не исключенный суффикс + public async Task ExecuteAsync_ShouldValidateExcludedSuffixes(string userName, bool shouldPass) + { + // Arrange + var requestPacket = CreateRadiusPacket(userName); + var ldapConfig = new LdapServerConfiguration + { + ExcludedSuffixes = new List { "domain.com", "example.com" } + }; + var context = CreateContext(requestPacket, ldapConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + if (shouldPass) + { + Assert.False(context.IsTerminated); + } + else + { + Assert.True(context.IsTerminated); + Assert.Equal("UPN suffix is not permitted.", context.ResponseInformation.ReplyMessage); + } + } + + [Fact] + public async Task ExecuteAsync_ShouldAllowWhenNoSuffixRestrictions() + { + // Arrange + var requestPacket = CreateRadiusPacket("user@anydomain.com"); + var ldapConfig = new LdapServerConfiguration + { + IncludedSuffixes = new List(), + ExcludedSuffixes = new List() + }; + var context = CreateContext(requestPacket, ldapConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + } + + [Fact] + public async Task ExecuteAsync_ShouldHandleEmptyUserName() + { + // Arrange + var requestPacket = CreateRadiusPacket(null); + var ldapConfig = new LdapServerConfiguration(); + var context = CreateContext(requestPacket, ldapConfig); + + // Act + await _step.ExecuteAsync(context); + + // Assert + Assert.False(context.IsTerminated); + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("User name is empty")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + private RadiusPacket CreateRadiusPacket(string userName) + { + var header = new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16]); + var packet = new RadiusPacket(header); + + if (userName != null) + { + packet.AddAttributeValue("User-Name", userName); + } + + return packet; + } + + private RadiusPipelineContext CreateContext(RadiusPacket requestPacket, ILdapServerConfiguration ldapConfig) + { + var clientConfig = new ClientConfiguration { Name = "TestClient" }; + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig); + return context; + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs new file mode 100644 index 00000000..40fc102c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs @@ -0,0 +1,370 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Core.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Radius +{ + public class RadiusPacketProcessorTests + { + private readonly Mock _pipelineProviderMock; + private readonly Mock _responseSenderMock; + private readonly Mock> _loggerMock; + private readonly RadiusPacketProcessor _processor; + + public RadiusPacketProcessorTests() + { + _pipelineProviderMock = new Mock(); + _responseSenderMock = new Mock(); + _loggerMock = new Mock>(); + _processor = new RadiusPacketProcessor( + _pipelineProviderMock.Object, + _responseSenderMock.Object, + _loggerMock.Object); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenPipelineProviderIsNull() + { + // Act & Assert + Assert.Throws(() => + new RadiusPacketProcessor(null, _responseSenderMock.Object, _loggerMock.Object)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenResponseSenderIsNull() + { + // Act & Assert + Assert.Throws(() => + new RadiusPacketProcessor(_pipelineProviderMock.Object, null, _loggerMock.Object)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenLoggerIsNull() + { + // Act & Assert + Assert.Throws(() => + new RadiusPacketProcessor(_pipelineProviderMock.Object, _responseSenderMock.Object, null)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldThrowArgumentNullException_WhenRequestPacketIsNull() + { + // Arrange + var clientConfig = new ClientConfiguration(); + + // Act & Assert + await Assert.ThrowsAsync(() => + _processor.ProcessPacketAsync(null, clientConfig)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldThrowArgumentNullException_WhenClientConfigurationIsNull() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + + // Act & Assert + await Assert.ThrowsAsync(() => + _processor.ProcessPacketAsync(packet, null)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldExecutePipelineWithoutLdap_WhenNoLdapServers() + { + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List() + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + await _processor.ProcessPacketAsync(packet, clientConfig); + + pipelineMock.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + _responseSenderMock.Verify(x => x.SendResponse(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldExecutePipelineWithoutLdap_WhenNotAccessRequest() + { + // Arrange + var packet = new Mock(); + packet.Setup(x => x.Code).Returns(PacketCode.AccountingRequest); // Not AccessRequest + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List + { + new LdapServerConfiguration { ConnectionString = "ldap://server1" } + } + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + // Act + await _processor.ProcessPacketAsync(packet.Object, clientConfig); + + // Assert + pipelineMock.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldTryAllLdapServers_WhenFirstFails() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List + { + new LdapServerConfiguration { ConnectionString = "ldap://server1" }, + new LdapServerConfiguration { ConnectionString = "ldap://server2" } + } + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + // First execution throws + var callCount = 0; + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(() => + { + callCount++; + if (callCount == 1) + throw new Exception("First server failed"); + }) + .Returns(Task.CompletedTask); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + pipelineMock.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Exactly(2)); + _responseSenderMock.Verify(x => x.SendResponse(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldThrowException_WhenAllLdapServersFail() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List + { + new LdapServerConfiguration { ConnectionString = "ldap://server1" }, + new LdapServerConfiguration { ConnectionString = "ldap://server2" } + } + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .ThrowsAsync(new Exception("LDAP server failed")); + + // Act & Assert + await Assert.ThrowsAsync(() => + _processor.ProcessPacketAsync(packet, clientConfig)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldStopTryingServers_WhenOneSucceeds() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List + { + new LdapServerConfiguration { ConnectionString = "ldap://server1" }, + new LdapServerConfiguration { ConnectionString = "ldap://server2" }, + new LdapServerConfiguration { ConnectionString = "ldap://server3" } + } + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + var callCount = 0; + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(() => + { + callCount++; + if (callCount == 2) // Second server succeeds + return; + if (callCount > 2) + throw new Exception("Should not reach third server"); + }) + .Returns(Task.CompletedTask); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + pipelineMock.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldThrowPipelineNotFoundException_WhenPipelineNotFound() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient" + }; + + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns((IRadiusPipeline)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _processor.ProcessPacketAsync(packet, clientConfig)); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldSendResponse_WhenPipelineExecutesSuccessfully() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List() + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + _responseSenderMock.Verify(x => x.SendResponse(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldCreateContextWithLdapServer_WhenLdapServerProvided() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + packet.AddAttributeValue("User-Password", "password"); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + RadiusSharedSecret = "secret", + LdapServers = new List + { + new LdapServerConfiguration { ConnectionString = "ldap://test-server" } + } + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + RadiusPipelineContext capturedContext = null; + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(ctx => capturedContext = ctx) + .Returns(Task.CompletedTask); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + Assert.NotNull(capturedContext); + Assert.NotNull(capturedContext.LdapConfiguration); + Assert.Equal("ldap://test-server", capturedContext.LdapConfiguration.ConnectionString); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldCreateContextWithoutLdapServer_WhenNoLdapServers() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + LdapServers = new List() + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + RadiusPipelineContext capturedContext = null; + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(ctx => capturedContext = ctx) + .Returns(Task.CompletedTask); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + Assert.NotNull(capturedContext); + Assert.Null(capturedContext.LdapConfiguration); + } + + [Fact] + public async Task ProcessPacketAsync_ShouldParsePassphrase_WithPreAuthenticationMethod() + { + // Arrange + var header = new RadiusPacketHeader(); + var packet = new RadiusPacket(header); + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + PreAuthenticationMethod = PreAuthMode.Otp, + LdapServers = new List() + }; + + var pipelineMock = new Mock(); + _pipelineProviderMock.Setup(x => x.GetPipeline(clientConfig)) + .Returns(pipelineMock.Object); + + RadiusPipelineContext capturedContext = null; + pipelineMock.Setup(x => x.ExecuteAsync(It.IsAny())) + .Callback(ctx => capturedContext = ctx) + .Returns(Task.CompletedTask); + + // Act + await _processor.ProcessPacketAsync(packet, clientConfig); + + // Assert + Assert.NotNull(capturedContext); + Assert.NotNull(capturedContext.Passphrase); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/ProtectionServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/ProtectionServiceTests.cs new file mode 100644 index 00000000..ce57413d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/ProtectionServiceTests.cs @@ -0,0 +1,200 @@ +using System.Security.Cryptography; +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Security; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Security +{ + public class ProtectionServiceTests + { + private const string TestSecret = "test-secret-123"; + private const string TestData = "test-data-to-protect"; + + [Fact] + public void Protect_ShouldThrowArgumentException_WhenDataIsNull() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Protect(TestSecret, null)); + } + + [Fact] + public void Protect_ShouldThrowArgumentException_WhenDataIsEmpty() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Protect(TestSecret, "")); + } + + [Fact] + public void Protect_ShouldThrowArgumentException_WhenDataIsWhitespace() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Protect(TestSecret, " ")); + } + + [Fact] + public void Unprotect_ShouldThrowArgumentException_WhenDataIsNull() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Unprotect(TestSecret, null)); + } + + [Fact] + public void Unprotect_ShouldThrowArgumentException_WhenDataIsEmpty() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Unprotect(TestSecret, "")); + } + + [Fact] + public void Unprotect_ShouldThrowArgumentException_WhenDataIsWhitespace() + { + // Act & Assert + Assert.Throws(() => ProtectionService.Unprotect(TestSecret, " ")); + } + + [Fact] + public void ProtectAndUnprotect_ShouldReturnOriginalData_OnWindows() + { + // Only run this test on Windows where ProtectedData is actually used + if (!OperatingSystem.IsWindows()) + return; + + // Arrange + var originalData = "sensitive-password-123!@#"; + + // Act + var protectedData = ProtectionService.Protect(TestSecret, originalData); + var unprotectResult = ProtectionService.Unprotect(TestSecret, protectedData); + + // Assert + Assert.NotNull(protectedData); + Assert.NotEmpty(protectedData); + Assert.NotEqual(originalData, protectedData); // Protected data should be different + Assert.Equal(originalData, unprotectResult); // Unprotect should return original + } + + [Fact] + public void Protect_ShouldReturnBase64String_OnNonWindows() + { + // Only run this test on non-Windows platforms + if (OperatingSystem.IsWindows()) + return; + + // Arrange + var originalData = "test-data"; + + // Act + var result = ProtectionService.Protect(TestSecret, originalData); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + // Should be valid base64 + var bytes = Convert.FromBase64String(result); + var decoded = Encoding.UTF8.GetString(bytes); + Assert.Equal(originalData, decoded); + } + + [Fact] + public void Unprotect_ShouldReturnOriginalData_OnNonWindows() + { + // Only run this test on non-Windows platforms + if (OperatingSystem.IsWindows()) + return; + + // Arrange + var originalData = "test-data"; + + // Act + var protectedData = ProtectionService.Protect(TestSecret, originalData); + var result = ProtectionService.Unprotect(TestSecret, protectedData); + + // Assert + Assert.Equal(originalData, result); + } + + [Fact] + public void ProtectAndUnprotect_ShouldHandleSpecialCharacters() + { + // Arrange + var originalData = "Password123!@#$%^&*()\n\t\r\0"; + + // Act + var protectedData = ProtectionService.Protect(TestSecret, originalData); + var result = ProtectionService.Unprotect(TestSecret, protectedData); + + // Assert + Assert.Equal(originalData, result); + } + + [Fact] + public void ProtectAndUnprotect_ShouldHandleUnicodeCharacters() + { + // Arrange + var originalData = "密码🔑пароль🎯"; + + // Act + var protectedData = ProtectionService.Protect(TestSecret, originalData); + var result = ProtectionService.Unprotect(TestSecret, protectedData); + + // Assert + Assert.Equal(originalData, result); + } + + [Fact] + public void ProtectAndUnprotect_ShouldHandleEmptySecret() + { + // Arrange + var secret = ""; + var originalData = "test-data"; + + // Act + var protectedData = ProtectionService.Protect(secret, originalData); + var result = ProtectionService.Unprotect(secret, protectedData); + + // Assert + Assert.Equal(originalData, result); + } + + [Fact] + public void ProtectAndUnprotect_ShouldWorkWithDifferentSecrets() + { + // Only run this test on Windows where ProtectedData uses the secret + if (!OperatingSystem.IsWindows()) + return; + + // Arrange + var secret1 = "secret-one"; + var secret2 = "secret-two"; + var originalData = "test-data"; + + // Act + var protectedWithSecret1 = ProtectionService.Protect(secret1, originalData); + + // Assert - Should fail with wrong secret on Windows + if (OperatingSystem.IsWindows()) + { + Assert.Throws(() => + ProtectionService.Unprotect(secret2, protectedWithSecret1)); + } + } + + [Fact] + public void Protect_ShouldReturnDifferentResultsForSameInput() + { + // Only run this test on Windows where ProtectedData adds entropy + if (!OperatingSystem.IsWindows()) + return; + + // Arrange + var data = "same-data"; + + // Act + var result1 = ProtectionService.Protect(TestSecret, data); + var result2 = ProtectionService.Protect(TestSecret, data); + + // Assert + Assert.NotEqual(result1, result2); // Should be different due to entropy + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/RadiusPasswordProtectorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/RadiusPasswordProtectorTests.cs new file mode 100644 index 00000000..73bacb88 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/RadiusPasswordProtectorTests.cs @@ -0,0 +1,179 @@ +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Security; + +namespace Multifactor.Radius.Adapter.v2.Tests.Application.Security +{ + public class RadiusPasswordProtectorTests + { + private readonly SharedSecret _sharedSecret; + private readonly RadiusAuthenticator _authenticator; + + public RadiusPasswordProtectorTests() + { + _sharedSecret = new SharedSecret("test-secret-123"); + _authenticator = new RadiusAuthenticator(new byte[16] { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + }); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPassword_ForShortPassword() + { + // Arrange + var password = "test123"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPassword_ForPasswordExactly16Chars() + { + // Arrange + var password = "1234567890123456"; // Exactly 16 chars + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPassword_ForPasswordLongerThan16Chars() + { + // Arrange + var password = "ThisIsAVeryLongPasswordThatExceeds16Characters"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPassword_ForPasswordWithSpecialCharacters() + { + // Arrange + var password = "P@ssw0rd!123#$\t\n\r"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPassword_ForUnicodePassword() + { + // Arrange + var password = "密码🔑пароль🎯"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_ShouldHandleEmptyPassword() + { + // Arrange + var password = ""; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); + } + + [Fact] + public void Decrypt_ShouldRemoveNullCharacters() + { + // Arrange + var password = "test"; + var passwordWithNulls = password + "\0\0\0\0\0"; + var passwordBytes = Encoding.UTF8.GetBytes(passwordWithNulls); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + var decrypted = RadiusPasswordProtector.Decrypt(_sharedSecret, _authenticator, encrypted); + + // Assert + Assert.Equal(password, decrypted); // Nulls should be removed + Assert.DoesNotContain(decrypted, "\0"); + } + + [Fact] + public void Encrypt_ShouldPadTo16ByteBoundaries() + { + // Arrange + var password = "short"; // 5 bytes + var passwordBytes = Encoding.UTF8.GetBytes(password); + + // Act + var encrypted = RadiusPasswordProtector.Encrypt(_sharedSecret, _authenticator, passwordBytes); + + // Assert + Assert.True(encrypted.Length % 16 == 0); // Should be multiple of 16 + Assert.Equal(16, encrypted.Length); // 5 padded to 16 + } + + [Fact] + public void Encrypt_ShouldProduceDifferentOutput_ForDifferentAuthenticators() + { + // Arrange + var password = "same-password"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + var auth1 = new RadiusAuthenticator(new byte[16]); + var auth2 = new RadiusAuthenticator(new byte[16] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }); + + // Act + var encrypted1 = RadiusPasswordProtector.Encrypt(_sharedSecret, auth1, passwordBytes); + var encrypted2 = RadiusPasswordProtector.Encrypt(_sharedSecret, auth2, passwordBytes); + + // Assert + Assert.NotEqual(encrypted1, encrypted2); + } + + [Fact] + public void Encrypt_ShouldProduceDifferentOutput_ForDifferentSharedSecrets() + { + // Arrange + var password = "password"; + var passwordBytes = Encoding.UTF8.GetBytes(password); + + var secret1 = new SharedSecret("secret1"); + var secret2 = new SharedSecret("secret2"); + var auth = new RadiusAuthenticator(new byte[16]); + + // Act + var encrypted1 = RadiusPasswordProtector.Encrypt(secret1, auth, passwordBytes); + var encrypted2 = RadiusPasswordProtector.Encrypt(secret2, auth, passwordBytes); + + // Assert + Assert.NotEqual(encrypted1, encrypted2); + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs deleted file mode 100644 index 2e63c1c4..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs +++ /dev/null @@ -1,941 +0,0 @@ -using System.Net; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests.ClientConfigurationFactoryTests; - -public class AppSettingsTests -{ - [Fact] - public void CreateClientConfiguration_FirstFactorIsNone_ShouldReturnConfiguration() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.Equal(AuthenticationSource.None, clientConfig.FirstFactorAuthenticationSource); - Assert.Equal("secret", clientConfig.RadiusSharedSecret); - Assert.NotNull(clientConfig.InvalidCredentialDelay); - Assert.Equal(configName, clientConfig.Name); - Assert.Equal("identifier", clientConfig.ApiCredential.Usr); - Assert.Equal("secret", clientConfig.ApiCredential.Pwd); - Assert.Equal("groups", clientConfig.SignUpGroups); - Assert.NotNull(clientConfig.PrivacyMode); - Assert.NotNull(clientConfig.PreAuthnMode); - Assert.True(clientConfig.BypassSecondFactorWhenApiUnreachable); - Assert.Equal("12345", clientConfig.CallingStationIdVendorAttribute); - Assert.NotNull(clientConfig.AuthenticationCacheLifetime); - Assert.Empty(clientConfig.NpsServerEndpoints); - Assert.Empty(clientConfig.RadiusReplyAttributes); - Assert.NotNull(clientConfig.UserNameTransformRules); - Assert.Null(clientConfig.ServiceClientEndpoint); - } - - [Fact] - public void CreateClientConfiguration_FirstFactorIsRadius_ShouldReturnConfiguration() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.Equal(AuthenticationSource.Radius, clientConfig.FirstFactorAuthenticationSource); - Assert.Equal("secret", clientConfig.RadiusSharedSecret); - Assert.NotNull(clientConfig.InvalidCredentialDelay); - Assert.Equal(configName, clientConfig.Name); - Assert.Equal("identifier", clientConfig.ApiCredential.Usr); - Assert.Equal("secret", clientConfig.ApiCredential.Pwd); - Assert.Equal("groups", clientConfig.SignUpGroups); - Assert.NotNull(clientConfig.PrivacyMode); - Assert.NotNull(clientConfig.PreAuthnMode); - Assert.True(clientConfig.BypassSecondFactorWhenApiUnreachable); - Assert.Equal("12345", clientConfig.CallingStationIdVendorAttribute); - Assert.NotNull(clientConfig.AuthenticationCacheLifetime); - Assert.NotNull(clientConfig.ServiceClientEndpoint); - Assert.NotNull(clientConfig.NpsServerEndpoints); - Assert.Single(clientConfig.NpsServerEndpoints); - var nps = clientConfig.NpsServerEndpoints.First(); - Assert.Equal(IPEndPoint.Parse("127.0.0.1"), nps); - Assert.Empty(clientConfig.RadiusReplyAttributes); - Assert.NotNull(clientConfig.UserNameTransformRules); - } - - [Theory] - [InlineData("invalid-nps-server")] - [InlineData("127.0.0.1; invalid-nps-server")] - [InlineData("127.0.0.1; invalid-nps-server; 127.0.0.2")] - public void CreateClientConfiguration_InvalidNpsServer_ShouldThrow(string npsSetting) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = npsSetting, - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var ex = Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - Assert.Contains("Invalid NPS", ex.Message); - } - - [Theory] - [InlineData("127.0.0.1:123")] - [InlineData("127.0.0.1; 127.0.0.2:123")] - [InlineData("127.0.0.1; 127.0.0.2; 127.0.0.3:123")] - public void CreateClientConfiguration_MultipleNpsServers_ShouldReturnConfiguration(string npsServers) - { - var expectedNpsServers = Utils.SplitString(npsServers).Select(IPEndPoint.Parse); - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = npsServers, - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.True(expectedNpsServers.SequenceEqual(clientConfig.NpsServerEndpoints)); - } - - [Fact] - public void CreateClientConfiguration_NpsServerTimeout_ShouldReturnTimeout() - { - var expectedNpsTimeout = TimeSpan.FromSeconds(30); - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = "127.0.0.1", - NpsServerTimeout = "00:00:30", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.Equal(expectedNpsTimeout, clientConfig.NpsServerTimeout); - } - - [Fact] - public void CreateClientConfiguration_NoNpsServerTimeout_ShouldReturnDefaultTimeout() - { - var expectedNpsTimeout = TimeSpan.FromSeconds(5); - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.Equal(expectedNpsTimeout, clientConfig.NpsServerTimeout); - } - - [Fact] - public void CreateClientConfiguration_InvalidNpsServerTimeout_ShouldThrow() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Radius", - AdapterClientEndpoint = "127.0.0.1", - NpsServerEndpoint = "127.0.0.1", - NpsServerTimeout = "random", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var ex = Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - Assert.Contains("Invalid NPS server timeout", ex.Message); - } - - [Fact] - public void CreateClientConfiguration_FirstFactorIsLdap_ShouldReturnConfiguration() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Ldap", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.NotNull(clientConfig); - Assert.Equal(AuthenticationSource.Ldap, clientConfig.FirstFactorAuthenticationSource); - Assert.Equal("secret", clientConfig.RadiusSharedSecret); - Assert.NotNull(clientConfig.InvalidCredentialDelay); - Assert.Equal(configName, clientConfig.Name); - Assert.Equal("identifier", clientConfig.ApiCredential.Usr); - Assert.Equal("secret", clientConfig.ApiCredential.Pwd); - Assert.Equal("groups", clientConfig.SignUpGroups); - Assert.NotNull(clientConfig.PrivacyMode); - Assert.NotNull(clientConfig.PreAuthnMode); - Assert.True(clientConfig.BypassSecondFactorWhenApiUnreachable); - Assert.Equal("12345", clientConfig.CallingStationIdVendorAttribute); - Assert.NotNull(clientConfig.AuthenticationCacheLifetime); - Assert.Empty(clientConfig.RadiusReplyAttributes); - Assert.NotNull(clientConfig.UserNameTransformRules); - Assert.Null(clientConfig.ServiceClientEndpoint); - } - - [Fact] - public void CreateClientConfiguration_FirstFactorIsLdapNoServers_ShouldThrow() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "Ldap", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("none")] - [InlineData("radius")] - public void CreateClientConfiguration_ReplyAttributesNoLdapServer_ShouldThrow(string firstFactor) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = firstFactor, - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - }, - RadiusReply = new RadiusReplySection() - { - Attributes = new RadiusReplyAttributesSection(new RadiusReplyAttribute() { Name = "name", From = "attr" }) - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyFirstFactor_ShouldThrow(string emptyString) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = emptyString, - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("123")] - [InlineData("windows")] - [InlineData("!2")] - public void CreateClientConfiguration_InvalidFirstFactor_ShouldThrow(string emptyString) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = emptyString, - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyRadiusSharedSecret_ShouldThrow(string emptyString) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = emptyString, - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyMultifactorNasIdentifier_ShouldThrow(string emptyString) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = emptyString, - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyMultifactorSharedSecret_ShouldThrow(string emptyString) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = emptyString, - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "Full", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("123")] - [InlineData("error")] - public void CreateClientConfiguration_InvalidPrivacyMode_ShouldThrow(string privacyMode) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = privacyMode, - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "3" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("-1")] - [InlineData("error")] - [InlineData("1-2-6")] - [InlineData("-1-1-6")] - public void CreateClientConfiguration_InvalidCredentialDelay_ShouldThrow(string delay) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = delay - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("$")] - [InlineData("?")] - [InlineData("!")] - public void CreateClientConfiguration_InvalidSignUpGroups_ShouldThrow(string groups) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = groups, - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = "00:01:00", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "1" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [InlineData("$")] - [InlineData("?")] - [InlineData("!")] - [InlineData("123")] - [InlineData("aa")] - [InlineData("00:00")] - public void CreateClientConfiguration_InvalidAuthenticationCacheLifetime_ShouldThrow(string lifetime) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "group", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - AuthenticationCacheLifetime = lifetime, - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "1" - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Fact] - public void CreateClientConfiguration_SingleValidWhiteIp_ShouldCreate() - { - var whiteList = "127.0.0.1"; - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "group", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "1", - IpWhiteList = whiteList - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - var expectedWhiteList = IPAddressRange.Parse(whiteList); - Assert.Equal(expectedWhiteList, config.IpWhiteList.First()); - } - - [Fact] - public void CreateClientConfiguration_MultipleValidWhiteIps_ShouldCreate() - { - var whiteList = "127.0.0.1; 127.0.0.2-128.0.0.1; 127.2.0.0/16"; - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "group", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "1", - IpWhiteList = whiteList - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - var expectedWhiteList = new[] { IPAddressRange.Parse("127.0.0.1"), IPAddressRange.Parse("127.0.0.2-128.0.0.1"), IPAddressRange.Parse("127.2.0.0/16") }; - Assert.True(expectedWhiteList.SequenceEqual(config.IpWhiteList)); - } - - [Fact] - public void CreateClientConfiguration_InvalidIpWhiteList_ShouldThrow() - { - var whiteList = "127.0.0.1; invalid-ip-address"; - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "nasIdentifier", - MultifactorSharedSecret = "Secret", - SignUpGroups = "group", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - RadiusSharedSecret = "secret", - PrivacyMode = "None", - PreAuthenticationMethod = "None", - CallingStationIdAttribute = "12345", - InvalidCredentialDelay = "1", - IpWhiteList = whiteList - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/LdapSettingsTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/LdapSettingsTests.cs deleted file mode 100644 index 4973844f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/LdapSettingsTests.cs +++ /dev/null @@ -1,867 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests.ClientConfigurationFactoryTests; - -public class LdapSettingsTests -{ - [Fact] - public void CreateClientConfiguration_ShouldReturnDefaultLdapServerConfiguration() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - Assert.NotNull(clientConfig); - Assert.NotNull(clientConfig.LdapServers); - Assert.NotEmpty(clientConfig.LdapServers); - var config = clientConfig.LdapServers[0]; - - Assert.Equal("connectionString", config.ConnectionString); - Assert.Equal("username", config.UserName); - Assert.Equal("password", config.Password); - - Assert.Empty(config.AccessGroups); - Assert.Empty(config.SecondFaGroups); - Assert.Empty(config.SecondFaBypassGroups); - Assert.Empty(config.NestedGroupsBaseDns); - Assert.Empty(config.PhoneAttributes); - Assert.True(config.LoadNestedGroups); - Assert.True(string.IsNullOrWhiteSpace(config.IdentityAttribute)); - Assert.Equal(30, config.BindTimeoutInSeconds); - } - - [Fact] - public void CreateClientConfiguration_ShouldReturnSingleLdapServerConfiguration() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - AccessGroups = "dc=groups", - SecondFaGroups = "dc=second fa groups", - SecondFaBypassGroups = "dc=second fa bypass groups", - LoadNestedGroups = true, - NestedGroupsBaseDn = "dc=nested groups", - PhoneAttributes = "phone attributes", - IdentityAttribute = "Id" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - var serverConfig = clientConfig.LdapServers.First(); - - Assert.Equal("connectionString", serverConfig.ConnectionString); - Assert.Equal("username", serverConfig.UserName); - Assert.Equal("password", serverConfig.Password); - Assert.Collection(serverConfig.AccessGroups, e => Assert.Equal(new DistinguishedName("dc=groups"), e)); - Assert.Collection(serverConfig.SecondFaGroups, e => Assert.Equal(new DistinguishedName("dc=second fa groups"), e)); - Assert.Collection(serverConfig.SecondFaBypassGroups, e => Assert.Equal(new DistinguishedName("dc=second fa bypass groups"), e)); - Assert.Collection(serverConfig.NestedGroupsBaseDns, e => Assert.Equal(new DistinguishedName("dc=nested groups"), e)); - Assert.Collection(serverConfig.PhoneAttributes, e => Assert.Equal("phone attributes", e)); - Assert.True(serverConfig.LoadNestedGroups); - Assert.Equal("Id", serverConfig.IdentityAttribute); - } - - [Fact] - public void CreateClientConfiguration_ShouldReturnTwoLdapServerConfigurations() - { - var ldapConfig = new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - AccessGroups = "dc=groups", - SecondFaGroups = "dc=second fa groups", - SecondFaBypassGroups = "dc=second fa bypass groups", - LoadNestedGroups = true, - NestedGroupsBaseDn = "dc=nested groups", - PhoneAttributes = "phone attributes", - IdentityAttribute = "Id" - }; - - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - ldapConfig, - ldapConfig - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - Assert.Equal(2, clientConfig.LdapServers.Count); - foreach (var serverConfig in clientConfig.LdapServers) - { - Assert.Equal("connectionString", serverConfig.ConnectionString); - Assert.Equal("username", serverConfig.UserName); - Assert.Equal("password", serverConfig.Password); - Assert.Collection(serverConfig.AccessGroups, e => Assert.Equal(new DistinguishedName("dc=groups"), e)); - Assert.Collection(serverConfig.SecondFaGroups, e => Assert.Equal(new DistinguishedName("dc=second fa groups"), e)); - Assert.Collection(serverConfig.SecondFaBypassGroups, e => Assert.Equal(new DistinguishedName("dc=second fa bypass groups"), e)); - Assert.Collection(serverConfig.NestedGroupsBaseDns, e => Assert.Equal(new DistinguishedName("dc=nested groups"), e)); - Assert.Collection(serverConfig.PhoneAttributes, e => Assert.Equal("phone attributes", e)); - Assert.True(serverConfig.LoadNestedGroups); - Assert.Equal("Id", serverConfig.IdentityAttribute); - } - } - - [Theory] - [InlineData("Ldap")] - public void CreateClientConfiguration_NoServerConfigs_ShouldThrow(string factor) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = factor, - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyConnectionString_ShouldThrow(string connection) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = connection, - UserName = "username", - Password = "password" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyUserName_ShouldThrow(string userName) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connection", - UserName = userName, - Password = "password" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void CreateClientConfiguration_EmptyPassword_ShouldThrow(string password) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connection", - UserName = "userName", - Password = password - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - //[Theory] placeholder for future - [InlineData("invalid-ip-address")] - [InlineData("1.1.1.1; invalid-ip-address")] - [InlineData("1.1.1.1; 2.2.2.2; invalid-ip-address")] - public void CreateClientConfiguration_InvalidIpWhiteList_ShouldThrow(string range) - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connection", - UserName = "userName", - Password = "password", - IpWhiteList = range - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - - var exception = Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - Assert.Contains("Invalid IP", exception.Message); - } - - //[Fact] - public void CreateClientConfiguration_SingleValidWhiteIp_ShouldCreate() - { - var range = "127.0.0.1"; - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connection", - UserName = "userName", - Password = "password", - IpWhiteList = range - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - Assert.Equal(IPAddressRange.Parse(range), config.LdapServers.First().IpWhiteList.First()); - } - - - //[Fact] - public void CreateClientConfiguration_MultipleValidWhiteIps_ShouldCreate() - { - var whiteList = "127.0.0.1; 127.0.0.2-128.0.0.1; 127.2.0.0/16"; - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - NpsServerEndpoint = "127.0.0.1" - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connection", - UserName = "userName", - Password = "password", - IpWhiteList = whiteList - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - var expectedWhiteList = new[] { IPAddressRange.Parse("127.0.0.1"), IPAddressRange.Parse("127.0.0.2-128.0.0.1"), IPAddressRange.Parse("127.2.0.0/16") }; - Assert.True(expectedWhiteList.SequenceEqual(config.LdapServers.First().IpWhiteList)); - } - - [Fact] - public void CreateClientConfiguration_AuthenticationCacheGroups_ShouldCreate() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - AuthenticationCacheGroups = "dc=group1;dc=group2 ;dc=group3; ; ;" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var clientConfig = factory.CreateConfig(configName, radiusConfig, serviceConfig); - - var serverConfig = clientConfig.LdapServers.First(); - - Assert.True(serverConfig.AuthenticationCacheGroups.SequenceEqual([new DistinguishedName("dc=group1"), new DistinguishedName("dc=group2"), new DistinguishedName("dc=group3")])); - } - - [Fact] - public void CreateClientConfiguration_SimultaneousUseOfDomainRules_ShouldThrow() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - IncludedDomains = "included domains", - ExcludedDomains = "excluded domains", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Fact] - public void CreateClientConfiguration_SimultaneousUseOfSuffixRules_ShouldThrow() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - IncludedSuffixes = "included suffixes", - ExcludedSuffixes = "excluded suffixes", - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Fact] - public void CreateClientConfiguration_EnableTrustedDomainsAndNoUpnRequirements_ShouldThrow() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - RequiresUpn = false, - EnableTrustedDomains = true - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - Assert.Throws(() => factory.CreateConfig(configName, radiusConfig, serviceConfig)); - } - - [Fact] - public void CreateClientConfiguration_IncludedDomains_ShouldSet() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - IncludedDomains = "included domains" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - var serverConfig = config.LdapServers.First(); - Assert.NotNull(serverConfig.DomainPermissions); - Assert.Collection(serverConfig.DomainPermissions.IncludedValues, e => Assert.Equal("included domains", e)); - } - - [Fact] - public void CreateClientConfiguration_ExcludedDomains_ShouldSet() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - ExcludedDomains = "excluded domains" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - var serverConfig = config.LdapServers.First(); - Assert.NotNull(serverConfig.DomainPermissions); - Assert.Collection(serverConfig.DomainPermissions.ExcludedValues, e => Assert.Equal("excluded domains", e)); - } - - [Fact] - public void CreateClientConfiguration_IncludedSuffixes_ShouldSet() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - IncludedSuffixes = "included suffixes" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - var serverConfig = config.LdapServers.First(); - Assert.NotNull(serverConfig.DomainPermissions); - Assert.Collection(serverConfig.SuffixesPermissions.IncludedValues, e => Assert.Equal("included suffixes", e)); - } - - [Fact] - public void CreateClientConfiguration_ExcludedSuffixes_ShouldSet() - { - var radiusConfig = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - MultifactorNasIdentifier = "identifier", - MultifactorSharedSecret = "secret", - SignUpGroups = "groups", - BypassSecondFactorWhenApiUnreachable = true, - FirstFactorAuthenticationSource = "None", - AdapterClientEndpoint = "127.0.0.1", - AdapterServerEndpoint = "127.0.0.1", - RadiusSharedSecret = "secret", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - ExcludedSuffixes = "excluded suffixes" - } - } - } - }; - - var serviceConfig = new ServiceConfiguration(); - var configName = "name"; - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - var factory = - new ClientConfigurationFactory(dictionaryMock.Object); - var config = factory.CreateConfig(configName, radiusConfig, serviceConfig); - var serverConfig = config.LdapServers.First(); - Assert.NotNull(serverConfig.DomainPermissions); - Assert.Collection(serverConfig.SuffixesPermissions.ExcludedValues, e => Assert.Equal("excluded suffixes", e)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs deleted file mode 100644 index e66fb007..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs +++ /dev/null @@ -1,258 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -public class LdapServerConfigurationTests -{ - [Fact] - public void CreateDefaultLdapServerConfiguration_ShouldCreate() - { - var connection = "connection"; - var user = "user"; - var password = "password"; - var config = new LdapServerConfiguration(connection, user, password); - - Assert.NotNull(config); - Assert.Equal(connection, config.ConnectionString); - Assert.Equal(user, config.UserName); - Assert.Equal(password, config.Password); - Assert.Empty(config.AccessGroups); - Assert.Empty(config.SecondFaGroups); - Assert.Empty(config.SecondFaBypassGroups); - Assert.Empty(config.NestedGroupsBaseDns); - Assert.Empty(config.PhoneAttributes); - Assert.False(config.LoadNestedGroups); - Assert.Null(config.IdentityAttribute); - Assert.Equal(0, config.BindTimeoutInSeconds); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void SetLoadNestedGroups_ShouldSet(bool loadNestedGroups) - { - var config = GetDefaultConfiguration(); - - config.SetLoadNestedGroups(loadNestedGroups); - - Assert.Equal(loadNestedGroups, config.LoadNestedGroups); - } - - [Fact] - public void SetIdentityAttribute_ShouldSet() - { - var config = GetDefaultConfiguration(); - var identity = "identity"; - config.SetIdentityAttribute(identity); - - Assert.Equal(identity, config.IdentityAttribute); - } - - [Fact] - public void SetBindTimeout_ShouldSet() - { - var config = GetDefaultConfiguration(); - var timeout = 10; - - config.SetBindTimeoutInSeconds(timeout); - - Assert.Equal(timeout, config.BindTimeoutInSeconds); - } - - [Fact] - public void SetBindTimeout_InvalidTimeout_ShouldThrow() - { - var config = GetDefaultConfiguration(); - var timeout = -1; - - Assert.Throws(() => config.SetBindTimeoutInSeconds(timeout)); - } - - [Theory] - [InlineData("dc=group")] - [InlineData("dc=group1;dc=group2")] - [InlineData("dc=group1;dc=group2;dc=group3")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4;")] - public void AddAccessGroups_ShouldAdd(string? groups) - { - var config = GetDefaultConfiguration(); - var expectedGroups = Utils.SplitString(groups); - - config.AddAccessGroups(expectedGroups); - - Assert.NotNull(config.AccessGroups); - Assert.True(expectedGroups.Select(x => new DistinguishedName(x)).SequenceEqual(config.AccessGroups)); - } - - [Theory] - [InlineData("dc=group")] - [InlineData("dc=group1;dc=group2")] - [InlineData("dc=group1;dc=group2;dc=group3")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4;")] - public void Add2FaGroups_ShouldAdd(string? groups) - { - var config = GetDefaultConfiguration(); - var expectedGroups = Utils.SplitString(groups); - config.AddSecondFaGroups(expectedGroups); - Assert.NotNull(config.SecondFaGroups); - Assert.True(expectedGroups.Select(x => new DistinguishedName(x)).SequenceEqual(config.SecondFaGroups)); - } - - [Theory] - [InlineData("dc=group")] - [InlineData("dc=group1;dc=group2")] - [InlineData("dc=group1;dc=group2;dc=group3")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4;")] - public void Add2FaBypassGroups_ShouldAdd(string? groups) - { - var config = GetDefaultConfiguration(); - var expectedGroups = Utils.SplitString(groups); - - config.AddSecondFaBypassGroups(expectedGroups); - - Assert.NotNull(config.SecondFaBypassGroups); - Assert.True(expectedGroups.Select(x => new DistinguishedName(x)).SequenceEqual(config.SecondFaBypassGroups)); - } - - [Theory] - [InlineData("group")] - [InlineData("group1;group2")] - [InlineData("group1;group2;group3")] - [InlineData("group1;group2;group3;group4")] - [InlineData("group1;group2;group3;group4;")] - public void AddPhoneAttributes_ShouldAdd(string? groups) - { - var config = GetDefaultConfiguration(); - var expectedGroups = Utils.SplitString(groups); - - config.AddPhoneAttributes(expectedGroups); - - Assert.NotNull(config.PhoneAttributes); - Assert.True(expectedGroups.SequenceEqual(config.PhoneAttributes)); - } - - [Theory] - [InlineData("dc=group")] - [InlineData("dc=group1;dc=group2")] - [InlineData("dc=group1;dc=group2;dc=group3")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4")] - [InlineData("dc=group1;dc=group2;dc=group3;dc=group4;")] - public void AddNestedGroupBaseDns_ShouldAdd(string? groups) - { - var config = GetDefaultConfiguration(); - var expectedGroups = Utils.SplitString(groups); - - config.AddNestedGroupBaseDns(expectedGroups); - - Assert.NotNull(config.NestedGroupsBaseDns); - Assert.True(expectedGroups.Select(x => new DistinguishedName(x)).SequenceEqual(config.NestedGroupsBaseDns)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void RequiresUpn_ShouldSet(bool value) - { - var config = GetDefaultConfiguration(); - - config.RequiresUpn(value); - - Assert.Equal(value, config.UpnRequired); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnableTrustedDomains_ShouldSetValue(bool value) - { - var config = GetDefaultConfiguration(); - - config.EnableTrustedDomains(value); - - Assert.Equal(value, config.TrustedDomainsEnabled); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnableAlternativeSuffixes_ShouldSetValue(bool value) - { - var config = GetDefaultConfiguration(); - - config.EnableTrustedDomains(value); - - Assert.Equal(value, config.TrustedDomainsEnabled); - } - - [Fact] - public void SetDomainRules_ShouldSet() - { - var config = GetDefaultConfiguration(); - - var rules = new PermissionRules(new List(), new List()); - config.SetDomainRules(rules); - - Assert.Equal(rules, config.DomainPermissions); - } - - [Fact] - public void SetAlternativeSuffixesRules_ShouldSet() - { - var config = GetDefaultConfiguration(); - var rules = new PermissionRules(new List(), new List()); - - config.SetAlternativeSuffixesRules(rules); - - Assert.Equal(rules, config.SuffixesPermissions); - } - - [Fact] - public void Initialize_ShouldInitialize() - { - var config = GetDefaultConfiguration(); - var rules = new PermissionRules(); - var request = new LdapServerInitializeRequest() - { - PhoneAttributes = ["phone"], - AccessGroups = [new DistinguishedName("dc=access")], - SecondFaGroups = [new DistinguishedName("dc=2fa")], - SecondFaBypassGroups = [new DistinguishedName("dc=2fabypass")], - NestedGroupsBaseDns = [new DistinguishedName("dc=nested")], - IdentityAttribute = "identity", - LoadNestedGroups = true, - BindTimeoutInSeconds = 10, - RequiresUpn = false, - EnableTrustedDomains = true, - EnableAlternativeSuffixes = true, - DomainPermissions = rules, - SuffixesPermissions = rules, - AuthenticationCacheGroups = [new DistinguishedName("dc=authentication")] - }; - - config.Initialize(request); - Assert.True(config.PhoneAttributes.SequenceEqual(request.PhoneAttributes)); - Assert.True(config.AccessGroups.SequenceEqual(request.AccessGroups)); - Assert.True(config.SecondFaGroups.SequenceEqual(request.SecondFaGroups)); - Assert.True(config.SecondFaBypassGroups.SequenceEqual(request.SecondFaBypassGroups)); - Assert.True(config.NestedGroupsBaseDns.SequenceEqual(request.NestedGroupsBaseDns)); - Assert.True(config.AuthenticationCacheGroups.SequenceEqual(request.AuthenticationCacheGroups)); - Assert.Equal(request.IdentityAttribute, config.IdentityAttribute); - Assert.Equal(request.LoadNestedGroups, config.LoadNestedGroups); - Assert.Equal(request.BindTimeoutInSeconds, config.BindTimeoutInSeconds); - Assert.Equal(request.RequiresUpn, config.UpnRequired); - Assert.Equal(request.EnableTrustedDomains, config.TrustedDomainsEnabled); - Assert.Equal(request.EnableAlternativeSuffixes, config.AlternativeSuffixesEnabled); - Assert.Equal(request.DomainPermissions, config.DomainPermissions); - Assert.Equal(request.SuffixesPermissions, config.SuffixesPermissions); - } - - private LdapServerConfiguration GetDefaultConfiguration() => new( - "connection", - "user", - "password"); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LoadXmlConfigurationTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LoadXmlConfigurationTests.cs deleted file mode 100644 index 60aea1b2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LoadXmlConfigurationTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -public class LoadXmlConfigurationTests -{ - [Fact] - public void LoadXmlConfig_ShouldLoadLdapServersSection() - { - var fileName = "full-single-file.config"; - - var configRoot = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(TestEnvironment.GetAssetPath(fileName))) - .Build(); - - var config = configRoot.BindRadiusAdapterConfig(); - - Assert.NotNull(config); - Assert.NotNull(config.LdapServers); - Assert.NotNull(config.LdapServers.Servers); - Assert.NotEmpty(config.LdapServers.Servers); - Assert.Equal(2, config.LdapServers.Servers.Length); - - Assert.Contains(config.LdapServers.Servers, x => - { - return - x.ConnectionString == "connection-string" && - x.UserName == "username" && - x.Password == "password" && - x.BindTimeoutInSeconds == 10 && - x.AccessGroups == "access-groups" && - x.SecondFaGroups == "2fa-groups" && - x.SecondFaBypassGroups == "2fa-bypass-groups" && - x.LoadNestedGroups == false && - x.NestedGroupsBaseDn == "nested-groups-base-dn" && - x.PhoneAttributes == "phone-attributes" && - x.IdentityAttribute == "identity-attribute"; - }); - - Assert.Contains(config.LdapServers.Servers, x => - { - return - x.ConnectionString == "connection-string" && - x.UserName == "username" && - x.Password == "password" && - x.BindTimeoutInSeconds == 10 && - x.AccessGroups == "access-groups" && - x.SecondFaGroups == "2fa-groups" && - x.SecondFaBypassGroups == "2fa-bypass-groups" && - x.LoadNestedGroups == true && - x.NestedGroupsBaseDn == "nested-groups-base-dn" && - x.PhoneAttributes == "phone-attributes" && - x.IdentityAttribute == "identity-attribute"; - }); - } - - [Fact] - public void LoadXmlConfig_ShouldLoadAppSettingsSection() - { - var fileName = "full-single-file.config"; - var configRoot = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(TestEnvironment.GetAssetPath(fileName))) - .Build(); - - var config = configRoot.BindRadiusAdapterConfig(); - - Assert.NotNull(config); - Assert.NotNull(config.AppSettings); - - Assert.Equal("first-factor-authentication-source", config.AppSettings.FirstFactorAuthenticationSource); - Assert.Equal("radius-shared-secret", config.AppSettings.RadiusSharedSecret); - Assert.Equal("multifactor-nas-identifier", config.AppSettings.MultifactorNasIdentifier); - Assert.Equal("multifactor-shared-secret", config.AppSettings.MultifactorSharedSecret); - Assert.Equal("adapter-client-endpoint", config.AppSettings.AdapterClientEndpoint); - Assert.Equal("adapter-server-endpoint", config.AppSettings.AdapterServerEndpoint); - Assert.Equal("nps-server-endpoint", config.AppSettings.NpsServerEndpoint); - Assert.Equal("radius-client-ip", config.AppSettings.RadiusClientIp); - Assert.Equal("radius-client-nas-identifier", config.AppSettings.RadiusClientNasIdentifier); - Assert.Equal("privacy-mode", config.AppSettings.PrivacyMode); - Assert.Equal("pre-authentication-method", config.AppSettings.PreAuthenticationMethod); - Assert.Equal("authentication-cache-lifetime", config.AppSettings.AuthenticationCacheLifetime); - Assert.Equal("invalid-credential-delay", config.AppSettings.InvalidCredentialDelay); - Assert.Equal("calling-station-id-attribute", config.AppSettings.CallingStationIdAttribute); - Assert.Equal("multifactor-api-url", config.AppSettings.MultifactorApiUrl); - Assert.Equal("multifactor-api-proxy", config.AppSettings.MultifactorApiProxy); - Assert.Equal("multifactor-api-timeout", config.AppSettings.MultifactorApiTimeout); - Assert.Equal("sign-up-groups", config.AppSettings.SignUpGroups); - Assert.True(config.AppSettings.BypassSecondFactorWhenApiUnreachable); - Assert.Equal("logging-level", config.AppSettings.LoggingLevel); - Assert.Equal("logging-format", config.AppSettings.LoggingFormat); - Assert.Equal("console-log-output-template", config.AppSettings.ConsoleLogOutputTemplate); - Assert.Equal("file-log-output-template", config.AppSettings.FileLogOutputTemplate); - } - - [Fact] - public void LoadXmlConfig_ShouldLoadRadiusReplyAttributes() - { - var fileName = "full-single-file.config"; - var configRoot = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(TestEnvironment.GetAssetPath(fileName))) - .Build(); - - var config = configRoot.BindRadiusAdapterConfig(); - - Assert.Equal(2, config.RadiusReply.Attributes.Elements.Length); - - Assert.Contains(config.RadiusReply.Attributes.Elements, x => - { - return x.Name == "Fortinet-Group-Name" && - x.Value == "Users" && - x.When == "UserGroup=VPN Users" && - x.Sufficient && - x.From == "from"; - }); - - Assert.Contains(config.RadiusReply.Attributes.Elements, x => - { - return x.Name == "Fortinet-Group-Name" && - x.Value == "Admins" && - x.When == "UserGroup=VPN Admins" && - !x.Sufficient && - x.From == "from"; - }); - } - - [Fact] - public void LoadXmlConfig_ShouldLoadUserNameTransformRules() - { - var fileName = "full-single-file.config"; - - var configRoot = new ConfigurationBuilder() - .Add(new XmlAppConfigurationSource(TestEnvironment.GetAssetPath(fileName))) - .Build(); - - var config = configRoot.BindRadiusAdapterConfig(); - Assert.Equal(2, config.UserNameTransformRules.Elements.Count()); - - Assert.Contains(config.UserNameTransformRules.Elements, x => - { - return x.Match == "^([^@]*)$" && - x.Replace == "$1@domain.local" && - x.Count == 3; - }); - - Assert.Contains(config.UserNameTransformRules.Elements, x => - { - return x.Match == "^([^@]*)$" && - x.Replace == "$1@domain.local" && - x.Count == 0; - }); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusAdapterConfigurationFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusAdapterConfigurationFactoryTests.cs deleted file mode 100644 index e5ef4269..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusAdapterConfigurationFactoryTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -public class RadiusAdapterConfigurationFactoryTests -{ - [Fact] - public void CreateMinimalRoot_WithNoEnvVar_ShouldCreate() - { - var path = TestEnvironment.GetAssetPath("root-minimal-single.config"); - var config = RadiusAdapterConfigurationFactory.Create(path); - - Assert.Equal("0.0.0.0:1812", config.AppSettings.AdapterServerEndpoint); - Assert.Equal("000", config.AppSettings.RadiusSharedSecret); - Assert.Equal("https://api.multifactor.dev", config.AppSettings.MultifactorApiUrl); - Assert.Equal("None", config.AppSettings.FirstFactorAuthenticationSource); - Assert.Equal("key", config.AppSettings.MultifactorNasIdentifier); - Assert.Equal("secret", config.AppSettings.MultifactorSharedSecret); - Assert.Equal("Debug", config.AppSettings.LoggingLevel); - } - - [Fact] - public void CreateMinimalRoot_OverrideByEnvVar_ShouldCreate() - { - TestEnvironmentVariables.With(env => - { - env.SetEnvironmentVariable("rad_appsettings__adapterServerEndpoint", "0.0.0.0:1818"); - env.SetEnvironmentVariable("rad_appsettings__RadiusSharedSecret", "888"); - env.SetEnvironmentVariable("rad_appsettings__MultifactorApiUrl", "https://api.multifactor.ru"); - env.SetEnvironmentVariable("rad_appsettings__FirstFactorAuthenticationSource", "ActiveDirectory"); - env.SetEnvironmentVariable("rad_appsettings__MultifactorNasIdentifier", "my key"); - env.SetEnvironmentVariable("rad_appsettings__MultifactorSharedSecret", "my secret"); - env.SetEnvironmentVariable("rad_appsettings__LoggingLevel", "Info"); - - var path = TestEnvironment.GetAssetPath("root-minimal-single.config"); - var config = RadiusAdapterConfigurationFactory.Create(path); - - Assert.Equal("0.0.0.0:1818", config.AppSettings.AdapterServerEndpoint); - Assert.Equal("888", config.AppSettings.RadiusSharedSecret); - Assert.Equal("https://api.multifactor.ru", config.AppSettings.MultifactorApiUrl); - Assert.Equal("ActiveDirectory", config.AppSettings.FirstFactorAuthenticationSource); - Assert.Equal("my key", config.AppSettings.MultifactorNasIdentifier); - Assert.Equal("my secret", config.AppSettings.MultifactorSharedSecret); - Assert.Equal("Info", config.AppSettings.LoggingLevel); - }); - } - - [Fact] - 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"); - - Assert.Equal("windows", config.AppSettings.RadiusClientNasIdentifier); - Assert.Equal("000", config.AppSettings.RadiusSharedSecret); - Assert.Equal("None", config.AppSettings.FirstFactorAuthenticationSource); - Assert.Equal("key", config.AppSettings.MultifactorNasIdentifier); - Assert.Equal("secret", config.AppSettings.MultifactorSharedSecret); - } - - [Fact] - public void CreateClient_OverrideByEnvVar_ShouldCreate() - { - TestEnvironmentVariables.With(env => - { - env.SetEnvironmentVariable("rad_client-minimal-for-overriding_appsettings__RadiusClientNasIdentifier", - "Linux"); - env.SetEnvironmentVariable("rad_client-minimal-for-overriding_appsettings__RadiusSharedSecret", - "888"); - env.SetEnvironmentVariable("rad_client-minimal-for-overriding_appsettings__FirstFactorAuthenticationSource", - "ActiveDirectory"); - env.SetEnvironmentVariable("rad_client-minimal-for-overriding_appsettings__MultifactorNasIdentifier", - "my key"); - env.SetEnvironmentVariable("rad_client-minimal-for-overriding_appsettings__MultifactorSharedSecret", - "my secret"); - - var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, - "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client-minimal-for-overriding"); - - Assert.Equal("Linux", config.AppSettings.RadiusClientNasIdentifier); - Assert.Equal("888", config.AppSettings.RadiusSharedSecret); - Assert.Equal("ActiveDirectory", config.AppSettings.FirstFactorAuthenticationSource); - Assert.Equal("my key", config.AppSettings.MultifactorNasIdentifier); - Assert.Equal("my secret", config.AppSettings.MultifactorSharedSecret); - }); - } - - [Fact] - public void CreateClientWithSpacedName_OverrideByEnvVar_ShouldCreate() - { - TestEnvironmentVariables.With(env => - { - env.SetEnvironmentVariable("rad_clientminimalspaced_appsettings__RadiusClientNasIdentifier", - "Linux"); - - var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, - "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client minimal spaced"); - - Assert.Equal("Linux", config.AppSettings.RadiusClientNasIdentifier); - }); - } - - [Fact] - public void CreateClient_ComplexPathOverrideByEnvVar_ShouldCreate() - { - TestEnvironmentVariables.With(env => - { - // Path = RadiusReply:Attributes:add:0:name, Value = Fortinet-Group-Name - env.SetEnvironmentVariable( - "rad_client-minimal-for-overriding_RadiusReply__Attributes__add__0__name", - "Fortinet-Group-Name"); - env.SetEnvironmentVariable( - "rad_client-minimal-for-overriding_RadiusReply__Attributes__add__0__value", - "Users"); - - var path = TestEnvironment.GetAssetPath(TestAssetLocation.ClientsDirectory, - "client-minimal-for-overriding.config"); - var config = RadiusAdapterConfigurationFactory.Create(path, "client-minimal-for-overriding"); - var attribute = Assert.Single(config.RadiusReply.Attributes.Elements); - Assert.NotNull(attribute); - - Assert.Equal("Fortinet-Group-Name", attribute.Name); - Assert.Equal("Users", attribute.Value); - }); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusConfigurationFileTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusConfigurationFileTests.cs deleted file mode 100644 index 8b61cdf2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusConfigurationFileTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -[Trait("Category", "App config Reading")] -public class RadiusConfigurationFileTests -{ - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("file")] - [InlineData("file.conf")] - public void Create_WrongPath_ShouldThrow(string path) - { - Assert.Throws(() => new RadiusConfigurationFile(path)); - } - - [Theory] - [InlineData("file.config")] - [InlineData("dir/file.config")] - [InlineData("/etc/configs/file.config")] - [InlineData("C:\\configs\\file.config")] - public void Create_CorrectPath_ShouldCreateAndStoreValue(string path) - { - var file = new RadiusConfigurationFile(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() - { - Assert.Throws(() => - { - RadiusConfigurationFile? file = null; - _ = (string)file; - }); - } - - [Fact] - public void Cast_ToStringFromCorrectRadConfFile_ShouldNotThrow() - { - RadiusConfigurationFile file = new("dir/file.config"); - var s = (string)file; - - Assert.Equal("dir/file.config", s); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs deleted file mode 100644 index 29b6977f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; -using LdapServerConfiguration = - Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer.LdapServerConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -public class ServiceConfigurationFactoryTests -{ - [Fact] - public void CreateServiceConfiguration_SingleConfig_ShouldCreate() - { - var clientConfigurationProviderMock = new Mock(); - clientConfigurationProviderMock.Setup(x => x.GetClientConfigurations()).Returns([]); - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - - var clientFactoryMock = new Mock(); - clientFactoryMock - .Setup( - x => x.CreateConfig( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(new Mock().Object); - - var serviceFactory = new ServiceConfigurationFactory( - clientConfigurationProviderMock.Object, - clientFactoryMock.Object, - NullLogger.Instance); - var serviceConfiguration = serviceFactory.CreateConfig(GetConfiguration()); - - Assert.NotNull(serviceConfiguration); - Assert.Equal("url", serviceConfiguration.ApiUrls[0]); - Assert.Equal("proxy", serviceConfiguration.ApiProxy); - Assert.Equal(TimeSpan.FromMinutes(2), serviceConfiguration.ApiTimeout); - Assert.True(serviceConfiguration.SingleClientMode); - Assert.NotNull(serviceConfiguration.InvalidCredentialDelay); - Assert.NotNull(serviceConfiguration.ServiceServerEndpoint); - Assert.Single(serviceConfiguration.Clients); - } - - [Fact] - public void CreateServiceConfiguration_NasIdentifierAsClientId_ShouldCreate() - { - var clientConfigurationProviderMock = new Mock(); - var clientAdapterConfig1 = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - RadiusClientNasIdentifier = "clientNasIdentifier1", - } - }; - - var clientAdapterConfig2 = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - RadiusClientNasIdentifier = "clientNasIdentifier2", - } - }; - - clientConfigurationProviderMock - .Setup(x => x.GetClientConfigurations()) - .Returns(new[] { clientAdapterConfig1, clientAdapterConfig2 }); - - clientConfigurationProviderMock - .Setup(x => x.GetSource(It.IsAny())) - .Returns(new FileMock()); - - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - - var clientFactoryMock = new Mock(); - - clientFactoryMock.Setup(x => x.CreateConfig(It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(new Mock().Object); - - var serviceFactory = new ServiceConfigurationFactory( - clientConfigurationProviderMock.Object, - clientFactoryMock.Object, - NullLogger.Instance); - - var serviceConfiguration = serviceFactory.CreateConfig(GetConfiguration()); - - Assert.NotNull(serviceConfiguration); - Assert.Equal("url", serviceConfiguration.ApiUrls[0]); - Assert.Equal("proxy", serviceConfiguration.ApiProxy); - Assert.Equal(TimeSpan.FromMinutes(2), serviceConfiguration.ApiTimeout); - Assert.False(serviceConfiguration.SingleClientMode); - Assert.NotNull(serviceConfiguration.InvalidCredentialDelay); - Assert.NotNull(serviceConfiguration.ServiceServerEndpoint); - Assert.Equal(2, serviceConfiguration.Clients.Count); - } - - [Fact] - public void CreateServiceConfiguration_IpAsClientId_ShouldCreate() - { - var clientConfigurationProviderMock = new Mock(); - var clientAdapterConfig1 = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - RadiusClientIp = "127.0.0.1", - } - }; - - var clientAdapterConfig2 = new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = "http://127.0.0.1", - RadiusClientNasIdentifier = "127.0.0.2", - } - }; - - clientConfigurationProviderMock - .Setup(x => x.GetClientConfigurations()) - .Returns(new[] { clientAdapterConfig1, clientAdapterConfig2 }); - - clientConfigurationProviderMock - .Setup(x => x.GetSource(It.IsAny())) - .Returns(new FileMock()); - - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - - var clientFactoryMock = new Mock(); - - clientFactoryMock - .Setup( - x => x.CreateConfig( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(new Mock().Object); - - var serviceFactory = new ServiceConfigurationFactory( - clientConfigurationProviderMock.Object, - clientFactoryMock.Object, - NullLogger.Instance); - - var serviceConfiguration = serviceFactory.CreateConfig(GetConfiguration()); - - Assert.NotNull(serviceConfiguration); - Assert.Equal("url", serviceConfiguration.ApiUrls[0]); - Assert.Equal("proxy", serviceConfiguration.ApiProxy); - Assert.Equal(TimeSpan.FromMinutes(2), serviceConfiguration.ApiTimeout); - Assert.False(serviceConfiguration.SingleClientMode); - Assert.NotNull(serviceConfiguration.InvalidCredentialDelay); - Assert.NotNull(serviceConfiguration.ServiceServerEndpoint); - Assert.Equal(2, serviceConfiguration.Clients.Count); - } - - [Theory] - [InlineData("url")] - [InlineData("url1;url2")] - [InlineData("url;url2;url3")] - public void CreateServiceConfiguration_MultipleMfApiUrls_ShouldCreate(string urls) - { - var clientConfigurationProviderMock = new Mock(); - clientConfigurationProviderMock.Setup(x => x.GetClientConfigurations()).Returns([]); - var dictionaryMock = new Mock(); - var attribute = new DictionaryAttribute("name", 1, "type"); - dictionaryMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(attribute); - - var clientFactoryMock = new Mock(); - clientFactoryMock - .Setup( - x => x.CreateConfig( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(new Mock().Object); - - var serviceFactory = new ServiceConfigurationFactory( - clientConfigurationProviderMock.Object, - clientFactoryMock.Object, - NullLogger.Instance); - var config = GetConfiguration(urls); - var serviceConfiguration = serviceFactory.CreateConfig(config); - - var expectedUrls = Utils.SplitString(urls); - var actualUrls = serviceConfiguration.ApiUrls; - Assert.True(expectedUrls.SequenceEqual(actualUrls)); - } - - private RadiusAdapterConfiguration GetConfiguration(string apiUrls = "url") => new RadiusAdapterConfiguration() - { - AppSettings = new AppSettingsSection() - { - MultifactorApiUrl = apiUrls, - MultifactorApiProxy = "proxy", - MultifactorApiTimeout = "00:02:00", - AdapterServerEndpoint = "127.0.0.1", - InvalidCredentialDelay = "3", - }, - LdapServers = new LdapServersSection() - { - LdapServers = new[] - { - new LdapServerConfiguration() - { - ConnectionString = "connectionString", - UserName = "username", - Password = "password", - } - } - } - }; - - private class FileMock : RadiusConfigurationSource - { - public override string Name => "File"; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationTests.cs deleted file mode 100644 index aebed6d6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Net; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; - -namespace Multifactor.Radius.Adapter.v2.Tests.ConfigurationTests; - -public class ServiceConfigurationTests -{ - [Fact] - public void BuildServiceConfiguration_ShouldBuild() - { - var configuration = new ServiceConfiguration(); - Assert.NotNull(configuration); - } - - [Fact] - public void SetApiProxy_ShouldSet() - { - var configuration = new ServiceConfiguration(); - configuration.SetApiProxy("proxy"); - - Assert.Equal("proxy", configuration.ApiProxy); - } - - [Fact] - public void SetApiUrl_ShouldSet() - { - var configuration = new ServiceConfiguration(); - configuration.AddApiUrl("url"); - Assert.Single(configuration.ApiUrls); - var apiUrl = configuration.ApiUrls[0]; - Assert.Equal("url", apiUrl); - } - - [Fact] - public void SetApiTimeout_ShouldSet() - { - var configuration = new ServiceConfiguration(); - var timeout = TimeSpan.FromSeconds(5); - configuration.SetApiTimeout(timeout); - - Assert.Equal(timeout, configuration.ApiTimeout); - } - - [Fact] - public void SetInvalidCredentialDelay_ShouldSet() - { - var configuration = new ServiceConfiguration(); - configuration.SetInvalidCredentialDelay(RandomWaiterConfig.Create("3")); - - Assert.NotNull(configuration.InvalidCredentialDelay); - } - - [Fact] - public void SetServiceServerEndpoint_ShouldSet() - { - var configuration = new ServiceConfiguration(); - IPEndPointFactory.TryParse("127.0.0.1", out var serviceServerEndpoint); - configuration.SetServiceServerEndpoint(serviceServerEndpoint); - - Assert.NotNull(configuration.ServiceServerEndpoint); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void SetIsSingleClientMode_ShouldSet(bool isSingleClientMode) - { - var configuration = new ServiceConfiguration(); - configuration.IsSingleClientMode(isSingleClientMode); - - Assert.Equal(isSingleClientMode, configuration.SingleClientMode); - } - - [Fact] - public void AddClientWithNasIdAsKey_ShouldAdd() - { - var configuration = new ServiceConfiguration(); - configuration.AddClient("key", new Mock().Object); - - Assert.Single(configuration.Clients); - Assert.NotNull(configuration.GetClient("key")); - } - - [Fact] - public void AddClientWithIpAsKey_ShouldAdd() - { - var configuration = new ServiceConfiguration(); - var key = IPAddress.Parse("127.0.0.1"); - configuration.AddClient(key, new Mock().Object); - - Assert.Single(configuration.Clients); - Assert.NotNull(configuration.GetClient(key)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs deleted file mode 100644 index ec35b92f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests; - -[Collection("LDAP")] -public class CustomLdapSchemaLoaderTests -{ - [Fact] - public void CustomLdapSchemaLoader_ShouldLoadSchema() - { - var config = GetConfig(); - var loader = new LdapSchemaLoader(LdapConnectionFactory.Create()); - var wrapper = new LdapSchemaLoaderWrapper(loader); - var customLdapSchemaLoader = new CustomLdapSchemaLoader(wrapper, NullLogger.Instance); - var connectionOptions = new LdapConnectionOptions( - new LdapConnectionString(config["ConnectionString"]), - AuthType.Basic, - config["UserName"], - config["Password"]); - - var schema = customLdapSchemaLoader.Load(connectionOptions); - - Assert.NotNull(schema); - Assert.Equal(LdapImplementation.ActiveDirectory, schema.LdapServerImplementation); - Assert.Equal(config["ExpectedDn"], schema.NamingContext.StringRepresentation); - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("LdapSchemaLoaderTests.txt", "|"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs deleted file mode 100644 index 4de82e6a..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - - -namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; - -[Collection("LDAP")] -public class LdapFirstFactorProcessorTests -{ - [Theory] - [InlineData("ActiveDirectoryCredentials.txt", "|", LdapImplementation.ActiveDirectory)] - //[InlineData("FreeIpaCredentials.txt", "|", LdapImplementation.FreeIPA)] - public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept(string config, string separator, LdapImplementation ldapImplementation) - { - //Arrange - var sensitiveData = GetConfig(config, separator); - var formatterProviderMock = new LdapBindNameFormatterProvider([new ActiveDirectoryFormatter(), new FreeIpaFormatter()]); - - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); - - var contextMock = new Mock(); - var packetMock = new Mock(); - packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - - var serverSettings = new Mock(); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - - var transformRules = new UserNameTransformRules(); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(sensitiveData["Password"], PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.LdapSchema.LdapServerImplementation).Returns(ldapImplementation); - var profile = new Mock(); - profile.Setup(x => x.Dn).Returns(new DistinguishedName(sensitiveData["UserDn"])); - contextMock.Setup(x => x.UserLdapProfile).Returns(profile.Object); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); - } - - [Theory] - [InlineData("ActiveDirectoryCredentials.txt", "|")] - [InlineData("FreeIpaCredentials.txt", "|")] - public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject(string config, string separator) - { - //Arrange - var sensitiveData = GetConfig(config, separator); - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Theory] - [InlineData("ActiveDirectoryCredentials.txt", "|")] - [InlineData("FreeIpaCredentials.txt", "|")] - public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject(string config, string separator) - { - //Arrange - var sensitiveData = GetConfig(config, separator); - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("userName"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - private Dictionary GetConfig(string config, string separator) - { - return ConfigUtils.GetConfigSensitiveData(config, separator); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs deleted file mode 100644 index 73ae06e2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; - -[Collection("LDAP")] -public class RadiusFirstFactorProcessorTests -{ - [Fact] - public async Task ProcessFirstFactor_ShouldAccept() - { - var sensitiveData = GetConfig(); - var factory = new RadiusClientFactory(NullLogger.Instance); - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret(sensitiveData["Secret"]); - var processor = new RadiusFirstFactorProcessor(packetService, factory, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var packetBytes = PacketExamples.DefaultAccessRequest; - var packet = packetService.Parse(packetBytes, secret); - - packet.ReplaceAttribute("User-Name", sensitiveData["UserName"]); - packet.ReplaceAttribute("User-Password", sensitiveData["Password"]); - - contextMock.Setup(x => x.RequestPacket).Returns(packet); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse(sensitiveData["NpsServerEndpoint"])])); - contextMock.Setup(x => x.NpsServerTimeout).Returns(TimeSpan.FromSeconds(5)); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse(sensitiveData["ServiceClientEndpoint"])); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(secret); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(sensitiveData["Password"], PreAuthModeDescriptor.Default)); - await processor.ProcessFirstFactor(contextMock.Object); - - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessFirstFactor_InvalidPassword_ShouldReject() - { - var sensitiveData = GetConfig(); - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret(sensitiveData["Secret"]); - var factory = new RadiusClientFactory(NullLogger.Instance); - var processor = new RadiusFirstFactorProcessor(packetService, factory, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var packetBytes = PacketExamples.DefaultAccessRequest; - var packet = packetService.Parse(packetBytes, secret); - - packet.ReplaceAttribute("User-Name", sensitiveData["UserName"]); - packet.ReplaceAttribute("User-Password", "pwd"); - - contextMock.Setup(x => x.RequestPacket).Returns(packet); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse(sensitiveData["NpsServerEndpoint"])])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse(sensitiveData["ServiceClientEndpoint"])); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(secret); - await processor.ProcessFirstFactor(contextMock.Object); - - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessFirstFactor_InvalidLogin_ShouldReject() - { - var sensitiveData = GetConfig(); - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret(sensitiveData["Secret"]); - var factory = new RadiusClientFactory(NullLogger.Instance); - var processor = new RadiusFirstFactorProcessor(packetService, factory, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var packetBytes = PacketExamples.DefaultAccessRequest; - var packet = packetService.Parse(packetBytes, secret); - - packet.ReplaceAttribute("User-Name", "user"); - packet.ReplaceAttribute("User-Password", sensitiveData["Password"]); - - contextMock.Setup(x => x.RequestPacket).Returns(packet); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse(sensitiveData["NpsServerEndpoint"])])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse(sensitiveData["ServiceClientEndpoint"])); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(secret); - await processor.ProcessFirstFactor(contextMock.Object); - - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("RadiusFirstFactorProcessorTests.txt", "|"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/ConfigUtils.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/ConfigUtils.cs deleted file mode 100644 index a4791e41..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/ConfigUtils.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Tests.Fixture; - -public static class ConfigUtils -{ - internal static Dictionary GetConfigSensitiveData(string fileName, string separator = ":") - { - var sensitiveDataPath = TestEnvironment.GetAssetPath(TestAssetLocation.SensitiveData, fileName); - - var lines = File.ReadLines(sensitiveDataPath); - var sensitiveData = new Dictionary(); - - foreach (var line in lines) - { - var parts = line.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (parts.Length != 2) - throw new ArgumentException($"Invalid sensitive data line: {line}"); - sensitiveData.Add(parts[0], parts[1]); - } - - return sensitiveData; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/EmptyStringsListInput.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/EmptyStringsListInput.cs deleted file mode 100644 index 711804ad..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/EmptyStringsListInput.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections; - -namespace Multifactor.Radius.Adapter.v2.Tests.Fixture; - -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(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/PacketExamples.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/PacketExamples.cs deleted file mode 100644 index 1280227d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/PacketExamples.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Tests.Fixture; - -public static class PacketExamples -{ - public static byte[] DefaultStatusServer = - { - 12, - 0, - 0, - 20, - 55, - 72, - 240, - 96, - 44, - 253, - 62, - 98, - 152, - 180, - 7, - 187, - 175, - 133, - 202, - 215, - }; - - public static byte[] DefaultAccessRequest = - { - 1, - 1, - 0, - 48, - 32, - 32, - 32, - 32, - 32, - 32, - 49, - 55, - 52, - 54, - 53, - 50, - 48, - 54, - 55, - 49, - 1, - 10, - 84, - 101, - 115, - 116, - 85, - 115, - 101, - 114, - 2, - 18, - 18, - 172, - 6, - 3, - 30, - 122, - 251, - 107, - 171, - 155, - 47, - 228, - 99, - 200, - 121, - 230 - }; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs deleted file mode 100644 index 4f0cdeab..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Reflection; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; - -namespace Multifactor.Radius.Adapter.v2.Tests.Fixture; - -internal static class TestUtils -{ - public static IRadiusDictionary GetRadiusDictionary(string? path = null) - { - var appVars = new ApplicationVariables() - { - AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), - AppVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(), - }; - - var dictionarySourcePath = path ?? $"{Path.DirectorySeparatorChar}Assets{Path.DirectorySeparatorChar}content{Path.DirectorySeparatorChar}radius.dictionary"; - var dictionary = new RadiusDictionary(appVars, dictionarySourcePath); - dictionary.Read(); - return dictionary; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapForest/LdapForestLoaderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapForest/LdapForestLoaderTests.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs deleted file mode 100644 index 2b95f6c4..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.DirectoryServices.Protocols; -using Moq; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests; - -[Collection("LDAP")] -public class LdapPasswordChangerTests -{ - [Fact] - public async Task ChangePassword_ShouldChange() - { - var factory = new CustomLdapConnectionFactory(); - - var sensitiveData = GetConfig(); - - var options = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["Admin"], - sensitiveData["AdminPwd"]); - - using var adminConnection = factory.CreateConnection(options); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var changer = new LdapPasswordChanger(adminConnection, schema); - var profileMock = new Mock(); - profileMock.Setup(x => x.Dn).Returns(new DistinguishedName(sensitiveData["UserDn"])); - var response = await changer.ChangeUserPasswordAsync(sensitiveData["NewPassword"], profileMock.Object); - - Assert.NotNull(response); - Assert.True(response.Success); - - var userConnectionOptions = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["UserName"], - sensitiveData["NewPassword"]); - using var newPasswordConnection = factory.CreateConnection(userConnectionOptions); - - //Rollback - response = await changer.ChangeUserPasswordAsync(sensitiveData["CurrentPassword"], profileMock.Object); - Assert.NotNull(response); - Assert.True(response.Success); - - userConnectionOptions = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["UserName"], - sensitiveData["CurrentPassword"]); - - using var oldPasswordConnection = factory.CreateConnection(userConnectionOptions); - } - - [Fact] - public async Task ChangePassword_UnsuccessfulResponseCode_ShouldFailed() - { - var factory = new CustomLdapConnectionFactory(); - - var sensitiveData = GetConfig(); - - var options = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["UserDn"], - sensitiveData["CurrentPassword"]); - - using var adminConnection = factory.CreateConnection(options); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var changer = new LdapPasswordChanger(adminConnection, schema); - var profileMock = new Mock(); - profileMock.Setup(x => x.Dn).Returns(new DistinguishedName(sensitiveData["UserDn"])); - var response = await changer.ChangeUserPasswordAsync(sensitiveData["NewPassword"], profileMock.Object); - - Assert.NotNull(response); - Assert.False(response.Success); - Assert.NotNull(response.Message); - Assert.NotEmpty(response.Message); - - var userConnectionOptions = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["UserName"], - sensitiveData["NewPassword"]); - - Assert.ThrowsAny(() => factory.CreateConnection(userConnectionOptions)); - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("ChangePasswordTests.txt", "|"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs deleted file mode 100644 index f054146d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.LdapProfile; - -[Collection("LDAP")] -public class LdapProfileLoaderTests -{ - [Fact] - public void LoadProfile_ShouldLoadProfile() - { - var factory = new CustomLdapConnectionFactory(); - - var sensitiveData = GetConfig(); - var options = new LdapConnectionOptions( - new LdapConnectionString(sensitiveData["ConnectionString"]), - AuthType.Basic, - sensitiveData["Admin"], - sensitiveData["AdminPwd"]); - using var connection = factory.CreateConnection(options); - - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var loader = new LdapProfileLoader(searchBase,connection,schema); - - var filter = $"(&(objectClass={sensitiveData["ObjectClass"]})({sensitiveData["IdentityAttribute1"]}={sensitiveData["TargetUserDn"]}))"; - var profile = loader.LoadLdapProfile(filter); - Assert.NotNull(profile); - var expectedDn = new DistinguishedName(sensitiveData["TargetUserDn"]); - Assert.Equal(expectedDn, profile.Dn); - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("LoadProfile.txt", "|"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs deleted file mode 100644 index 5394b700..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.LdapProfile; - -[Collection("LDAP")] -public class LdapProfileServiceTests -{ - [Fact] - public void LoadProfile_ByDn_ShouldLoadProfile() - { - var sensitiveData = GetConfig(); - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var targetUser = new UserIdentity(sensitiveData["TargetUserDn"]); - var serverConfig = GetServerConfig(sensitiveData); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var service = new LdapProfileService(new CustomLdapConnectionFactory(), NullLogger.Instance); - var ldapProfile = service.FindUserProfile(new FindUserProfileRequest("clientKey", serverConfig, schema, searchBase, targetUser)); - - Assert.NotNull(ldapProfile); - - var expectedUserDn = sensitiveData["TargetUserDn"].ToLower(); - var actualDn = ldapProfile.Dn.StringRepresentation.ToLower(); - Assert.Equal(expectedUserDn, actualDn); - } - - [Fact] - public void LoadProfile_ByUpn_ShouldLoadProfile() - { - var sensitiveData = GetConfig(); - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var targetUser = new UserIdentity(sensitiveData["TargetUserUpn"]); - var serverConfig = GetServerConfig(sensitiveData); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var service = new LdapProfileService(new CustomLdapConnectionFactory(), NullLogger.Instance); - var ldapProfile = service.FindUserProfile(new FindUserProfileRequest("clientKey", serverConfig, schema, searchBase, targetUser)); - - Assert.NotNull(ldapProfile); - - var expectedUserDn = sensitiveData["TargetUserDn"].ToLower(); - var actualDn = ldapProfile.Dn.StringRepresentation.ToLower(); - Assert.Equal(expectedUserDn, actualDn); - } - - [Fact] - public void LoadProfile_ByUid_ShouldLoadProfile() - { - var sensitiveData = GetConfig(); - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var targetUser = new UserIdentity(sensitiveData["TargetUserUid"]); - var serverConfig = GetServerConfig(sensitiveData); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var service = new LdapProfileService(new CustomLdapConnectionFactory(), NullLogger.Instance); - var ldapProfile = service.FindUserProfile(new FindUserProfileRequest("clientKey", serverConfig, schema, searchBase, targetUser)); - - Assert.NotNull(ldapProfile); - - var expectedUserDn = sensitiveData["TargetUserDn"].ToLower(); - var actualDn = ldapProfile.Dn.StringRepresentation.ToLower(); - Assert.Equal(expectedUserDn, actualDn); - } - - [Fact] - public void LoadProfile_ByNetBios_ShouldLoadProfile() - { - var sensitiveData = GetConfig(); - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var targetUser = new UserIdentity(sensitiveData["TargetUserNetBios"]); - var serverConfig = GetServerConfig(sensitiveData); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.ActiveDirectory; - var service = new LdapProfileService(new CustomLdapConnectionFactory(), NullLogger.Instance); - var ldapProfile = service.FindUserProfile(new FindUserProfileRequest("clientKey", serverConfig, schema, searchBase, targetUser)); - - Assert.NotNull(ldapProfile); - - var expectedUserDn = sensitiveData["TargetUserDn"].ToLower(); - var actualDn = ldapProfile.Dn.StringRepresentation.ToLower(); - Assert.Equal(expectedUserDn, actualDn); - } - - private ILdapServerConfiguration GetServerConfig(Dictionary sensitiveData) - { - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverConfigMock.Setup(x => x.UserName).Returns(sensitiveData["Admin"]); - serverConfigMock.Setup(x => x.Password).Returns(sensitiveData["AdminPwd"]); - serverConfigMock.Setup(x => x.BindTimeoutInSeconds).Returns(30); - return serverConfigMock.Object; - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("LoadProfileService.txt", "|"); - } -} - -[Collection("LDAP")] -public class FreeIpaLdapProfileServiceTests -{ - //[Fact] - public void LoadProfile_ByUid_ShouldLoadProfile() - { - var sensitiveData = GetConfig(); - var searchBase = new DistinguishedName(sensitiveData["SearchBase"]); - var targetUser = new UserIdentity(sensitiveData["TargetUserUid"]); - var serverConfig = GetServerConfig(sensitiveData); - var schema = LdapSchemaBuilder.Create(); - schema.LdapServerImplementation = LdapImplementation.FreeIPA; - var service = new LdapProfileService(new CustomLdapConnectionFactory(), NullLogger.Instance); - var ldapProfile = service.FindUserProfile(new FindUserProfileRequest("clientKey", serverConfig, schema, searchBase, targetUser)); - - Assert.NotNull(ldapProfile); - - var expectedUserDn = sensitiveData["TargetUserDn"].ToLower(); - var actualDn = ldapProfile.Dn.StringRepresentation.ToLower(); - Assert.Equal(expectedUserDn, actualDn); - } - - private ILdapServerConfiguration GetServerConfig(Dictionary sensitiveData) - { - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverConfigMock.Setup(x => x.UserName).Returns(sensitiveData["Admin"]); - serverConfigMock.Setup(x => x.Password).Returns(sensitiveData["AdminPwd"]); - serverConfigMock.Setup(x => x.BindTimeoutInSeconds).Returns(30); - return serverConfigMock.Object; - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("FreeIpaUserProfile.txt", "|"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs deleted file mode 100644 index 98c5b4bb..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Core.Ldap.Entry; -using Multifactor.Core.Ldap.Name; - -namespace Multifactor.Radius.Adapter.v2.Tests.LdapProfile; - -[Collection("LDAP")] -public class LdapProfileTest -{ - [Fact] - public void CreateLdapProfile_EntryIsNull_ThrowsArgumentNullException() - { - Assert.Throws(() => new Core.Ldap.LdapProfile(null)); - } - - [Fact] - public void CreateLdapProfile_ShouldCreateLdapProfile() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var attributes = new LdapAttribute[] - { - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - Assert.NotNull(profile); - Assert.Equal(dn, profile.Dn); - } - - [Fact] - public void CreateLdapProfile_MemberOfAttribute_ShouldReturnMemberOf() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var group1 = "cn=group1,dc=example,dc=com"; - var group2 = "cn=group2,dc=example,dc=com"; - var attributes = new LdapAttribute[] - { - new(new LdapAttributeName("memberOf"), [group1, group2]), - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var memberOf = profile.MemberOf.OrderBy(x =>x.StringRepresentation); - Assert.NotNull(memberOf); - var expected = new[] { new DistinguishedName(group1), new DistinguishedName(group2) }.OrderBy(x =>x.StringRepresentation); - Assert.True(expected.SequenceEqual(memberOf)); - } - - [Fact] - public void CreateLdapProfile_UpnAttribute_ShouldReturnUpn() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var upn = "user@domain.com"; - - var attributes = new LdapAttribute[] - { - new(new LdapAttributeName("userPrincipalName"), [upn]), - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var upnFromProfile = profile.Upn; - Assert.Equal(upn, upnFromProfile); - } - - [Fact] - public void CreateLdapProfile_PhoneAttribute_ShouldReturnPhone() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var phone = "somephone"; - - var attributes = new LdapAttribute[] - { - new(new LdapAttributeName("mobile"), [phone]), - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var phoneFromProfile = profile.Phone; - Assert.Equal(phone, phoneFromProfile); - } - - [Fact] - public void CreateLdapProfile_EmailAttribute_ShouldReturnEmail() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var email = "someEmail"; - - var attributes = new LdapAttribute[] - { - new(new LdapAttributeName("email"), [email]), - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var emailFromProfile = profile.Email; - Assert.NotNull(emailFromProfile); - Assert.Equal(email, emailFromProfile); - } - - [Fact] - public void CreateLdapProfile_MailAttribute_ShouldReturnMail() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var email = "someEmail"; - - var attributes = new LdapAttribute[] - { - new(new LdapAttributeName("mail"), [email]), - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var emailFromProfile = profile.Email; - Assert.NotNull(emailFromProfile); - Assert.Equal(email, emailFromProfile); - } - - [Fact] - public void CreateLdapProfile_GetAttributes_ShouldReturnAttributes() - { - var dn = new DistinguishedName("dc=example,dc=com"); - var email = "someEmail"; - var phone = "somePhone"; - var attr1 = new LdapAttribute(new LdapAttributeName("email"), [email]); - var attr2 = new LdapAttribute(new LdapAttributeName("phone"), [phone]); - var attributes = new LdapAttribute[] - { - attr1, - attr2 - }; - var ldapAttrCollection = new LdapAttributeCollection(attributes); - var entry = new LdapEntry(dn, ldapAttrCollection); - - var profile = new Core.Ldap.LdapProfile(entry); - - var attributesFromProfile = profile.Attributes; - Assert.Equal(2, attributesFromProfile.Count); - Assert.Contains(attr1, attributesFromProfile); - Assert.Contains(attr2, attributesFromProfile); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Multifactor.Radius.Adapter.v2.Tests.csproj b/src/Multifactor.Radius.Adapter.v2.Tests/Multifactor.Radius.Adapter.v2.Tests.csproj index bbf8bdbf..00fb4afd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Multifactor.Radius.Adapter.v2.Tests.csproj +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Multifactor.Radius.Adapter.v2.Tests.csproj @@ -21,10 +21,6 @@ - - - - Always @@ -49,5 +45,10 @@ Always + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/NetBiosServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/NetBiosServiceTests.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs deleted file mode 100644 index a29d19f3..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; - -public class BuildPipelineTests -{ - [Fact] - public void NoPipelineSteps_ShouldReturnPipeline() - { - var pipelineBuilder = new PipelineBuilder(); - var pipeline = pipelineBuilder.Build(); - Assert.NotNull(pipeline); - } - - [Fact] - public void ShouldBuildPipeline() - { - var mock1 = new Mock(); - var mock2 = new Mock(); - var pipelineBuilder = new PipelineBuilder(); - pipelineBuilder - .AddPipelineStep(mock1.Object) - .AddPipelineStep(mock2.Object); - var pipeline = pipelineBuilder.Build(); - Assert.NotNull(pipeline); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs deleted file mode 100644 index d2bc2288..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Diagnostics; -using Moq; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Xunit.Abstractions; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; - - -public class PerformanceTests -{ - private readonly ITestOutputHelper _testOutputHelper; - - public PerformanceTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - [Theory] - [InlineData(5)] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - public void PipelineTest(int stepsCount) - { - var builder = new PipelineBuilder(); - for (int i = 0; i < stepsCount; i++) - { - builder.AddPipelineStep(new StepMock()); - } - - var pipeline = builder.Build(); - var sw = Stopwatch.StartNew(); - pipeline.ExecuteAsync(new Mock().Object); - sw.Stop(); - _testOutputHelper.WriteLine(sw.Elapsed.ToString()); - } - - [Theory] - [InlineData(5)] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public void ForTest(int stepsCount) - { - var steps = new List(stepsCount); - for (int i = 0; i < stepsCount; i++) - { - steps.Add(new StepMock()); - } - var sw = Stopwatch.StartNew(); - foreach (var step in steps) - { - step.ExecuteAsync(new Mock().Object); - } - - sw.Stop(); - _testOutputHelper.WriteLine(sw.Elapsed.ToString()); - } - - private class StepMock : IRadiusPipelineStep - { - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs deleted file mode 100644 index e59f0858..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Services.Cache; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; - -public class PipelineConfigurationFactoryTests -{ - [Fact] - public void CreatePipelineConfiguration_ShouldReturnConfiguration() - { - var config = new PipelineStepsConfiguration("name", PreAuthMode.None); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var factory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = factory.CreatePipelineConfiguration(config); - - Assert.NotNull(pipelineConfiguration); - Assert.NotEmpty(pipelineConfiguration.PipelineStepsTypes); - Assert.All(pipelineConfiguration.PipelineStepsTypes, Assert.NotNull); - } - - [Fact] - public void BuildPipelineConfiguration_ShouldReturnDefaultConfig() - { - var config = new PipelineStepsConfiguration("name", PreAuthMode.None, hasLdapServers: true); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(UserNameValidationStep).IsAssignableFrom(e)), - e => Assert.True(typeof(LdapSchemaLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(ProfileLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessGroupsCheckingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e))); - } - - [Fact] - public void BuildPipelineConfiguration_GroupLoading_ShouldReturnConfig() - { - var config = new PipelineStepsConfiguration("name", PreAuthMode.None, true, true); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(UserNameValidationStep).IsAssignableFrom(e)), - e => Assert.True(typeof(LdapSchemaLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(ProfileLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessGroupsCheckingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(UserGroupLoadingStep).IsAssignableFrom(e))); - } - - [Theory] - [InlineData(PreAuthMode.Otp)] - [InlineData(PreAuthMode.Any)] - public void BuildPipelineConfiguration_ShouldReturnPreAuthConfiguration(PreAuthMode mode) - { - var config = new PipelineStepsConfiguration("name", mode, hasLdapServers: true); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(UserNameValidationStep).IsAssignableFrom(e)), - e => Assert.True(typeof(LdapSchemaLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(ProfileLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessGroupsCheckingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(PreAuthCheckStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(PreAuthPostCheck).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e))); - } - - [Fact] - public void BuildPipelineConfiguration_ShouldReturnConfigurationWithoutMembership() - { - var config = new PipelineStepsConfiguration("name", PreAuthMode.None, hasLdapServers: true); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(UserNameValidationStep).IsAssignableFrom(e)), - e => Assert.True(typeof(LdapSchemaLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(ProfileLoadingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessGroupsCheckingStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e))); - } - - [Fact] - public void BuildPipelineConfiguration_NoLdapServers_ShouldReturnConfig() - { - var config = new PipelineStepsConfiguration("name", PreAuthMode.None, hasLdapServers: false); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e))); - } - - - [Theory] - [InlineData(PreAuthMode.Otp)] - [InlineData(PreAuthMode.Any)] - public void BuildPipelineConfiguration_NoLdapServersWithPreAuth_ShouldReturnConfig(PreAuthMode mode) - { - var config = new PipelineStepsConfiguration("name", mode, hasLdapServers: false); - - var cache = new Mock(); - var outVal = new PipelineConfiguration([]); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var pipelineConfigurationFactory = new PipelineConfigurationFactory(cache.Object); - - var pipelineConfiguration = pipelineConfigurationFactory.CreatePipelineConfiguration(config); - Assert.Collection( - pipelineConfiguration.PipelineStepsTypes, - e => Assert.True(typeof(StatusServerFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(IpWhiteListStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessRequestFilteringStep).IsAssignableFrom(e)), - e => Assert.True(typeof(AccessChallengeStep).IsAssignableFrom(e)), - e => Assert.True(typeof(PreAuthCheckStep).IsAssignableFrom(e)), - e => Assert.True(typeof(SecondFactorStep).IsAssignableFrom(e)), - e => Assert.True(typeof(PreAuthPostCheck).IsAssignableFrom(e)), - e => Assert.True(typeof(FirstFactorStep).IsAssignableFrom(e))); - } - - public class Entry : ICacheEntry - { - public void Dispose() - { - } - - public object Key { get; } - public object? Value { get; set; } - public DateTimeOffset? AbsoluteExpiration { get; set; } - public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } - public TimeSpan? SlidingExpiration { get; set; } - public IList ExpirationTokens { get; } - public IList PostEvictionCallbacks { get; } - public CacheItemPriority Priority { get; set; } - public long? Size { get; set; } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs deleted file mode 100644 index a7346dfe..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; - -public class PipelineExecutionTests -{ - [Fact] - public async Task ShouldExecuteEmptyPipeline() - { - var pipelineBuilder = new PipelineBuilder(); - - var pipeline = pipelineBuilder.Build(); - var contextMock = new Mock(); - contextMock.Setup(x => x.ExecutionState).Returns(new ExecutionState()); - var context = contextMock.Object; - await pipeline.ExecuteAsync(context); - } - - [Fact] - public async Task ShouldExecutePipelineInRightOrder() - { - var executionChain = new List(3); - var pipelineBuilder = new PipelineBuilder(); - pipelineBuilder - .AddPipelineStep(new StepMock(1,executionChain)) - .AddPipelineStep(new StepMock(2,executionChain)) - .AddPipelineStep(new StepMock(3,executionChain)); - - var pipeline = pipelineBuilder.Build(); - - var contextMock = new Mock(); - contextMock.Setup(x => x.ExecutionState).Returns(new ExecutionState()); - var context = contextMock.Object; - await pipeline.ExecuteAsync(context); - - Assert.Equal(3, executionChain.Count); - Assert.Collection(executionChain, - e => Assert.Equal(1, e), - e => Assert.Equal(2, e), - e => Assert.Equal(3, e)); - } - - private class StepMock : IRadiusPipelineStep - { - private readonly int _step; - private readonly List _stepChain; - public StepMock(int stepNumber, List stepChain) - { - _step = stepNumber; - _stepChain = stepChain; - } - - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _stepChain.Add(_step); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs deleted file mode 100644 index a1546ce0..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Services.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class AccessGroupsCheckingStepTests -{ - [Fact] - public async Task CheckAccessGroups_NoAccessGroups_ShouldComplete() - { - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - var execState = new ExecutionState(); - serverConfigMock.Setup(x => x.AccessGroups).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(() => new Mock().Object); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - await step.ExecuteAsync(context); - - Assert.False(execState.IsTerminated); - Assert.False(execState.ShouldSkipResponse); - groupService.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); - } - - [Fact] - public async Task CheckAccessGroups_NoContext_ShouldThrow() - { - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - - await Assert.ThrowsAsync(() => step.ExecuteAsync(null)); - } - - [Fact] - public async Task CheckAccessGroups_NoLdapServerConfiguration_ShouldThrow() - { - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(()=> null); - var context = contextMock.Object; - - await Assert.ThrowsAsync(() => step.ExecuteAsync(context)); - } - - [Fact] - public async Task CheckAccessGroups_NoUserLdapProfile_ShouldThrow() - { - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.AccessGroups).Returns([new DistinguishedName("dc=group")]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => null); - contextMock.Setup(x => x.LdapSchema).Returns(() => new Mock().Object); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - - await Assert.ThrowsAsync(() => step.ExecuteAsync(context)); - } - - [Fact] - public async Task CheckAccessGroups_NoLdapSchema_ShouldThrow() - { - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(() => null); - var context = contextMock.Object; - - await Assert.ThrowsAsync(() => step.ExecuteAsync(context)); - } - - [Fact] - public async Task CheckAccessGroups_IsNotMember_ShouldTerminatePipeline() - { - var groupService = new Mock(); - groupService.Setup(x => x.IsMemberOf(It.IsAny())).Returns(false); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - var execState = new ExecutionState(); - serverConfigMock.Setup(x => x.AccessGroups).Returns([new DistinguishedName("dc=group,dc=admin,dc=user")]); - serverConfigMock.Setup(x => x.NestedGroupsBaseDns).Returns([]); - serverConfigMock.Setup(x => x.ConnectionString).Returns("string"); - serverConfigMock.Setup(x => x.UserName).Returns("string"); - serverConfigMock.Setup(x => x.Password).Returns("string"); - serverConfigMock.Setup(x => x.BindTimeoutInSeconds).Returns(23); - - var profileMock = new Mock(); - profileMock.Setup(x => x.Dn).Returns(new DistinguishedName("cn=admin,dc=admin,dc=user")); - profileMock.Setup(x => x.MemberOf).Returns([]); - - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => profileMock.Object); - contextMock.Setup(x => x.LdapSchema).Returns(() => new Mock().Object); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - - await step.ExecuteAsync(context); - - Assert.True(execState.IsTerminated); - Assert.False(execState.ShouldSkipResponse); - } - - [Fact] - public async Task CheckAccessGroups_IsMember_ShouldNotTerminatePipeline() - { - var groupService = new Mock(); - groupService.Setup(x => x.IsMemberOf(It.IsAny())).Returns(true); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - - serverConfigMock.Setup(x => x.AccessGroups).Returns([new DistinguishedName("dc=group,dc=admin,dc=user")]); - serverConfigMock.Setup(x => x.NestedGroupsBaseDns).Returns([]); - serverConfigMock.Setup(x => x.ConnectionString).Returns("string"); - serverConfigMock.Setup(x => x.UserName).Returns("string"); - serverConfigMock.Setup(x => x.Password).Returns("string"); - serverConfigMock.Setup(x => x.BindTimeoutInSeconds).Returns(23); - - var profileMock = new Mock(); - profileMock.Setup(x => x.Dn).Returns(new DistinguishedName("cn=admin,dc=admin,dc=user")); - profileMock.Setup(x => x.MemberOf).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => profileMock.Object); - contextMock.Setup(x => x.LdapSchema).Returns(() => new Mock().Object); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - - var execState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - await step.ExecuteAsync(context); - - Assert.False(execState.IsTerminated); - Assert.False(execState.ShouldSkipResponse); - } - - [Fact] - public async Task CheckAccessGroups_NotDomainAccount_ShouldSkipGroupCheck() - { - //Arrange - var groupService = new Mock(); - var step = new AccessGroupsCheckingStep(groupService.Object, NullLogger.Instance); - var contextMock = new Mock(); - var serverConfigMock = new Mock(); - var execState = new ExecutionState(); - serverConfigMock.Setup(x => x.AccessGroups).Returns([new DistinguishedName("dc=group")]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.UserLdapProfile).Returns(() => new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(() => new Mock().Object); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - contextMock.Setup(x => x.IsDomainAccount).Returns(false); - var packetMock = new Mock(); - packetMock.Setup(x=> x.AccountType).Returns(AccountType.Unknown); - packetMock.Setup(x=> x.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.False(execState.IsTerminated); - Assert.False(execState.ShouldSkipResponse); - groupService.Verify(x => x.LoadUserGroups(It.IsAny()), Times.Never); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs deleted file mode 100644 index 01c3f22e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class AccessRequestFilteringStepTests -{ - [Fact] - public async Task Execute_AccessRequestPacket_ShouldExecuteStep() - { - var context = GetContextMock(PacketCode.AccessRequest); - var statusServerFilteringStep = new AccessRequestFilteringStep(NullLogger.Instance); - await statusServerFilteringStep.ExecuteAsync(context); - - Assert.False(context.ExecutionState.IsTerminated); - Assert.False(context.ExecutionState.ShouldSkipResponse); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.SecondFactorStatus); - } - - [Fact] - public async Task Execute_NotAccessRequestPacket_ShouldTerminatePipeline() - { - var context = GetContextMock(PacketCode.CoaRequest); - var statusServerFilteringStep = new AccessRequestFilteringStep(NullLogger.Instance); - await statusServerFilteringStep.ExecuteAsync(context); - - Assert.True(context.ExecutionState.IsTerminated); - Assert.True(context.ExecutionState.ShouldSkipResponse); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.SecondFactorStatus); - } - - private IRadiusPipelineExecutionContext GetContextMock(PacketCode packetCode) - { - var packetMock = new Mock(); - packetMock.Setup(x => x.Code).Returns(packetCode); - - var authState = new AuthenticationState(); - var responseInformation = new ResponseInformation(); - var execState = new ExecutionState(); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInformation); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - return contextMock.Object; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs deleted file mode 100644 index 0f103100..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class PreAuthCheckStepTests -{ - [Fact] - public async Task OptModeWithoutOpt_ShouldTerminatePipeline() - { - var contextMock = new Mock(); - var preAuth = PreAuthModeDescriptor.Create("otp", new PreAuthModeSettings(10)); - var execState = new ExecutionState(); - contextMock.Setup(x => x.PreAuthnMode).Returns(preAuth); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", preAuth)); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var step = new PreAuthCheckStep(NullLogger.Instance); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Reject, context.AuthenticationState.SecondFactorStatus); - Assert.True(execState.IsTerminated); - } - - [Theory] - [InlineData("None")] - [InlineData("Otp")] - [InlineData("Any")] - public async Task CorrectPreAuthState_ShouldBypass(string mode) - { - var contextMock = new Mock(); - var preAuth = PreAuthModeDescriptor.Create(mode, new PreAuthModeSettings(1)); - var execState = new ExecutionState(); - contextMock.Setup(x => x.PreAuthnMode).Returns(preAuth); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", preAuth)); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - - var step = new PreAuthCheckStep(NullLogger.Instance); - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.SecondFactorStatus); - Assert.False(execState.IsTerminated); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs deleted file mode 100644 index cc9e190e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class PreAuthPostCheckStepTests -{ - [Theory] - [InlineData(AuthenticationStatus.Accept)] - [InlineData(AuthenticationStatus.Bypass)] - public async Task SuccessfulSecondFactor_ShouldBypass(AuthenticationStatus status) - { - var contextMock = new Mock(); - var execState = new ExecutionState(); - contextMock.Setup(x => x.AuthenticationState.SecondFactorStatus).Returns(status); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("Test"); - contextMock.Setup(x => x.LdapSchema).Returns(LdapSchemaBuilder.Default); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - - var context = contextMock.Object; - var step = new PreAuthPostCheck(NullLogger.Instance); - await step.ExecuteAsync(context); - Assert.False(execState.IsTerminated); - } - - [Theory] - [InlineData(AuthenticationStatus.Reject)] - [InlineData(AuthenticationStatus.Awaiting)] - public async Task UnsuccessfulSecondFactor_ShouldTerminatePipeline(AuthenticationStatus status) - { - var contextMock = new Mock(); - var execState = new ExecutionState(); - contextMock.Setup(x => x.AuthenticationState.SecondFactorStatus).Returns(status); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("Test"); - contextMock.Setup(x => x.LdapSchema).Returns(LdapSchemaBuilder.Default); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - - var context = contextMock.Object; - var step = new PreAuthPostCheck(NullLogger.Instance); - await step.ExecuteAsync(context); - Assert.True(execState.IsTerminated); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs deleted file mode 100644 index e28c1c86..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class ProfileLoadingStepTests -{ - [Fact] - public async Task ExecStep_ShouldLoadProfile() - { - var loaderMock = new Mock(); - var profile = new LdapProfileMock(); - loaderMock - .Setup(x => x.FindUserProfile(It.IsAny())) - .Returns(profile); - var schemaMock = new Mock(); - schemaMock.Setup(x => x.NamingContext).Returns(new DistinguishedName("dc=test,dc=example,dc=com")); - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("user@example.com"); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.LdapSchema).Returns(schemaMock.Object); - var serverConfig = new Mock(); - serverConfig.Setup(x => x.PhoneAttributes).Returns([]); - serverConfig.Setup(x => x.UserProfileCacheLifeTimeInHours).Returns(1); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfig.Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.SetupProperty(x => x.UserLdapProfile); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - var cacheMock = new Mock(); - - var step = new ProfileLoadingStep(loaderMock.Object, cacheMock.Object, NullLogger.Instance); - await step.ExecuteAsync(context); - - Assert.NotNull(context.UserLdapProfile); - Assert.Equal(profile.Dn, context.UserLdapProfile.Dn); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task ExecStep_NoUserName_ShouldDoNothing(string userName) - { - var loaderMock = new Mock(); - var profile = new LdapProfileMock(); - loaderMock - .Setup(x => x.FindUserProfile(It.IsAny())) - .Returns(profile); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns(userName); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:666")); - contextMock.SetupProperty(x => x.UserLdapProfile); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - var cacheMock = new Mock(); - var step = new ProfileLoadingStep(loaderMock.Object, cacheMock.Object, NullLogger.Instance); - await step.ExecuteAsync(context); - - Assert.Null(context.UserLdapProfile); - } - - [Fact] - public async Task ExecStep_NoLdapProfile_ShouldThrow() - { - var loaderMock = new Mock(); - loaderMock - .Setup(x => x.FindUserProfile(It.IsAny())) - .Returns(() => null); - var schemaMock = new Mock(); - schemaMock.Setup(x => x.NamingContext).Returns(new DistinguishedName("dc=test,dc=example,dc=com")); - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("user@example.com"); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.LdapSchema).Returns(schemaMock.Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - var serverConfig = new Mock(); - serverConfig.Setup(x => x.PhoneAttributes).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverConfig.Object); - contextMock.SetupProperty(x => x.UserLdapProfile); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - var cacheMock = new Mock(); - var step = new ProfileLoadingStep(loaderMock.Object, cacheMock.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => step.ExecuteAsync(context)); - } - - [Fact] - public async Task ExecStep_NoLdapSchema_ShouldDoNothing() - { - var loaderMock = new Mock(); - loaderMock - .Setup(x => x.FindUserProfile(It.IsAny())) - .Returns(() => null); - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("user@example.com"); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.LdapSchema).Returns(() => null); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.UserLdapProfile); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - var context = contextMock.Object; - var cacheMock = new Mock(); - var step = new ProfileLoadingStep(loaderMock.Object, cacheMock.Object, NullLogger.Instance); - await step.ExecuteAsync(context); - - Assert.Null(context.UserLdapProfile); - } - - [Fact] - public async Task ExecStep_NotDomainAccount_ShouldSkipStep() - { - //Arrange - var loaderMock = new Mock(); - loaderMock - .Setup(x => x.FindUserProfile(It.IsAny())) - .Returns(() => null); - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("user@example.com"); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("name"); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.UserLdapProfile); - contextMock.Setup(x => x.IsDomainAccount).Returns(false); - - var context = contextMock.Object; - var cacheMock = new Mock(); - var step = new ProfileLoadingStep(loaderMock.Object, cacheMock.Object, NullLogger.Instance); - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.Null(context.UserLdapProfile); - loaderMock.Verify(x => x.FindUserProfile(It.IsAny()), Times.Never); - } - - private class LdapProfileMock : ILdapProfile - { - public DistinguishedName Dn { get; } - public string? Upn { get; } - public string? Phone { get; } - public string? Email { get; } - public string? DisplayName { get; } - public IReadOnlyCollection MemberOf { get; } - public IReadOnlyCollection Attributes { get; } - - public LdapProfileMock() - { - MemberOf = []; - Attributes = []; - Dn = new DistinguishedName("dc=test,dc=example,dc=com"); - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs deleted file mode 100644 index 2ff01422..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs +++ /dev/null @@ -1,457 +0,0 @@ -# nullable disable -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class SecondFactorStepTests -{ - [Fact] - public void EmptyContext_ShouldThrowArgumentNullException() - { - var apiServiceMock = new Mock(); - var challengeProviderMock = new Mock(); - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - Assert.ThrowsAsync(() => step.ExecuteAsync(null)); - } - - [Fact] - public async Task ExecuteAsync_AclRequest_ShouldSetBypass() - { - var apiServiceMock = new Mock(); - var challengeProviderMock = new Mock(); - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(true); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Bypass, context.AuthenticationState.SecondFactorStatus); - } - - [Theory] - [InlineData(AuthenticationStatus.Bypass)] - [InlineData(AuthenticationStatus.Reject)] - [InlineData(AuthenticationStatus.Accept)] - [InlineData(AuthenticationStatus.Awaiting)] - public async Task ExecuteAsync_NoBypass_ShouldSecondFactorStatus(AuthenticationStatus apiResponseStatus) - { - var apiServiceMock = new Mock(); - apiServiceMock.Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())).ReturnsAsync(new MultifactorResponse(apiResponseStatus, "state", "message")); - - var challengeProviderMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(new Mock().Object); - - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns(new List()); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - await step.ExecuteAsync(context); - - Assert.Equal(apiResponseStatus, context.AuthenticationState.SecondFactorStatus); - Assert.Equal("state", context.ResponseInformation.State); - Assert.Equal("message", context.ResponseInformation.ReplyMessage); - } - - [Fact] - public async Task ExecuteAsync_AwaitingResponse_ShouldAddChallengeContext() - { - var apiServiceMock = new Mock(); - apiServiceMock - .Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())) - .ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting, "state", "message")); - - var challengeProviderMock = new Mock(); - var processorMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(processorMock.Object); - - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x=> x.IsDomainAccount).Returns(true); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns(new List()); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.SecondFactorStatus); - Assert.Equal("state", context.ResponseInformation.State); - Assert.Equal("message", context.ResponseInformation.ReplyMessage); - processorMock.Verify(x => x.AddChallengeContext(context), Times.Once); - } - - [Fact] - public async Task ExecuteAsync_MemberOf2FaBypassGroups_ShouldBypass() - { - var apiServiceMock = new Mock(); - var challengeProviderMock = new Mock(); - var groupServiceMock = new Mock(); - groupServiceMock.Setup(x => x.IsMemberOf(It.IsAny())).Returns(true); - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List() { new ("dc=bypass, dc=group") }); - ldapConfig.Setup(x => x.LoadNestedGroups).Returns(false); - ldapConfig.Setup(x => x.NestedGroupsBaseDns).Returns([]); - ldapConfig.Setup(x => x.ConnectionString).Returns("string"); - ldapConfig.Setup(x => x.UserName).Returns("username"); - ldapConfig.Setup(x => x.Password).Returns("password"); - ldapConfig.Setup(x => x.BindTimeoutInSeconds).Returns(30); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("dc=bypass,dc=group,dc=member")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([]); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Bypass, context.AuthenticationState.SecondFactorStatus); - } - - [Fact] - public async Task ExecuteAsync_NotMemberOf2FaGroups_ShouldBypass() - { - var apiServiceMock = new Mock(); - var challengeProviderMock = new Mock(); - var groupServiceMock = new Mock(); - groupServiceMock.Setup(x => x.IsMemberOf(It.IsAny())).Returns(false); - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List() { new("dc=bypass, dc=group") }); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - ldapConfig.Setup(x => x.LoadNestedGroups).Returns(false); - ldapConfig.Setup(x => x.NestedGroupsBaseDns).Returns([]); - ldapConfig.Setup(x => x.ConnectionString).Returns("string"); - ldapConfig.Setup(x => x.UserName).Returns("username"); - ldapConfig.Setup(x => x.Password).Returns("password"); - ldapConfig.Setup(x => x.BindTimeoutInSeconds).Returns(30); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("dc=bypass,dc=group,dc=member")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([]); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Bypass, context.AuthenticationState.SecondFactorStatus); - } - - [Fact] - public async Task ExecuteAsync_NoDomainAccount_ShouldSkipGroupCheck() - { - //Arrange - var apiServiceMock = new Mock(); - apiServiceMock.Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())).ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting, "state", "message")); - - var challengeProviderMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(new Mock().Object); - - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - contextMock.Setup(x=> x.IsDomainAccount).Returns(false); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List() { new("dc=group") }); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List() { new("dc=group") }); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns([new("dc=group")]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.Equal("state", context.ResponseInformation.State); - Assert.Equal("message", context.ResponseInformation.ReplyMessage); - groupServiceMock.Verify(x=> x.IsMemberOf(It.IsAny()), Times.Never); - apiServiceMock.Verify(x=> x.CreateSecondFactorRequestAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async Task ExecuteAsync_NoDomainAccount_ShouldSkipApiResponseCache() - { - //Arrange - var apiServiceMock = new Mock(); - apiServiceMock.Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())).ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting, "state", "message")); - - var challengeProviderMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(new Mock().Object); - - var groupServiceMock = new Mock(); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - contextMock.Setup(x=> x.IsDomainAccount).Returns(false); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List() { new("dc=group") }); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List() { new("dc=group") }); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns([new DistinguishedName("dc=group")]); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - //Act - await step.ExecuteAsync(context); - - //Assert - apiServiceMock.Verify(x=> x.CreateSecondFactorRequestAsync(It.Is(r => r.ApiResponseCacheEnabled == false)), Times.Once); - } - - [Fact] - public async Task ExecuteAsync_NotMemberOfAuthenticationCacheGroups_ShouldSkipApiResponseCache() - { - //Arrange - var apiServiceMock = new Mock(); - apiServiceMock.Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())).ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting, "state", "message")); - - var challengeProviderMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(new Mock().Object); - - var groupServiceMock = new Mock(); - var cacheGroup = new DistinguishedName("dc=Authentication,dc=Cache,dc=Groups"); - groupServiceMock.Setup(x => x.IsMemberOf(It.Is(r => r.TargetGroups.Contains(cacheGroup)))).Returns(false); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - contextMock.Setup(x=> x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns([new DistinguishedName("dc=Authentication,dc=Cache,dc=Groups")]); - ldapConfig.Setup(x => x.NestedGroupsBaseDns).Returns(new List()); - ldapConfig.Setup(x => x.ConnectionString).Returns("127.0.0.1"); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - //Act - await step.ExecuteAsync(context); - - //Assert - apiServiceMock.Verify(x=> x.CreateSecondFactorRequestAsync(It.Is(r => r.ApiResponseCacheEnabled == false)), Times.Once); - } - - [Fact] - public async Task ExecuteAsync_MemberOfAuthenticationCacheGroups_ShouldCacheApiResponse() - { - //Arrange - var apiServiceMock = new Mock(); - apiServiceMock.Setup(x => x.CreateSecondFactorRequestAsync(It.IsAny())).ReturnsAsync(new MultifactorResponse(AuthenticationStatus.Awaiting, "state", "message")); - - var challengeProviderMock = new Mock(); - challengeProviderMock.Setup(x => x.GetChallengeProcessorByType(ChallengeType.SecondFactor)).Returns(new Mock().Object); - - var groupServiceMock = new Mock(); - var cacheGroup = new DistinguishedName("dc=Authentication,dc=Cache,dc=Groups"); - groupServiceMock.Setup(x => x.IsMemberOf(It.Is(r => r.TargetGroups.Contains(cacheGroup)))).Returns(true); - - var step = new SecondFactorStep(apiServiceMock.Object, challengeProviderMock.Object, groupServiceMock.Object, NullLogger.Instance); - - var packetMock = new Mock(); - packetMock.Setup(x => x.IsVendorAclRequest).Returns(false); - var contextMock = new Mock(); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.FirstFactorAuthenticationSource).Returns(AuthenticationSource.Radius); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:1")); - contextMock.SetupProperty(x => x.ResponseInformation); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("1","2")); - contextMock.Setup(x=> x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var ldapConfig = new Mock(); - ldapConfig.Setup(x => x.SecondFaGroups).Returns(new List()); - ldapConfig.Setup(x => x.SecondFaBypassGroups).Returns(new List()); - ldapConfig.Setup(x => x.AuthenticationCacheGroups).Returns([new DistinguishedName("dc=Authentication,dc=Cache,dc=Groups")]); - ldapConfig.Setup(x => x.NestedGroupsBaseDns).Returns(new List()); - ldapConfig.Setup(x => x.ConnectionString).Returns("127.0.0.1"); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(ldapConfig.Object); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.ResponseInformation = new ResponseInformation(); - - //Act - await step.ExecuteAsync(context); - - //Assert - apiServiceMock.Verify(x=> x.CreateSecondFactorRequestAsync(It.Is(r => r.ApiResponseCacheEnabled == true)), Times.Once); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs deleted file mode 100644 index 80025ca5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class StatusServerFilteringStepTests -{ - [Fact] - public async Task Execute_StatusServerPacket_ShouldExecuteStep() - { - var context = GetContextMock(PacketCode.StatusServer); - var statusServerFilteringStep = new StatusServerFilteringStep(new ApplicationVariables(), NullLogger.Instance); - await statusServerFilteringStep.ExecuteAsync(context); - - Assert.StartsWith("Server up", context.ResponseInformation.ReplyMessage); - Assert.True(context.ExecutionState.IsTerminated); - Assert.Equal(AuthenticationStatus.Accept, context.AuthenticationState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Accept, context.AuthenticationState.SecondFactorStatus); - } - - [Fact] - public async Task Execute_NotStatusServer_ShouldSkipStep() - { - var context = GetContextMock(PacketCode.CoaAck); - var statusServerFilteringStep = new StatusServerFilteringStep(new ApplicationVariables(), NullLogger.Instance); - await statusServerFilteringStep.ExecuteAsync(context); - - Assert.Null(context.ResponseInformation.ReplyMessage); - Assert.False(context.ExecutionState.IsTerminated); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, context.AuthenticationState.SecondFactorStatus); - } - - private IRadiusPipelineExecutionContext GetContextMock(PacketCode packetCode) - { - var authState = new AuthenticationState(); - var responseInformation = new ResponseInformation(); - var execState = new ExecutionState(); - var packetMock = new Mock(); - packetMock.Setup(x => x.Code).Returns(packetCode); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInformation); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - - return contextMock.Object; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs deleted file mode 100644 index 3c5cefd3..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs +++ /dev/null @@ -1,281 +0,0 @@ -# nullable disable -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; - -public class UserGroupLoadingStepTests -{ - [Fact] - public async Task LoadGroups_NoReplyAttributes_ShouldSkipGroupLoading() - { - var groupService = new Mock(); - var connectionFactory = new Mock(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - contextMock.SetupProperty(x => x.UserGroups); - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.Empty(context.UserGroups); - } - - [Fact] - public async Task LoadGroups_NoRequiredAttributes_ShouldSkipGroupLoading() - { - var groupService = new Mock(); - var connectionFactory = new Mock(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("name", string.Empty)]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - contextMock.SetupProperty(x => x.UserGroups); - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.Empty(context.UserGroups); - groupService.Verify(x=> x.LoadUserGroups(It.IsAny()), Times.Never); - } - - [Theory] - [InlineData(AuthenticationStatus.Awaiting, AuthenticationStatus.Awaiting)] - [InlineData(AuthenticationStatus.Reject, AuthenticationStatus.Reject)] - [InlineData(AuthenticationStatus.Awaiting, AuthenticationStatus.Reject)] - [InlineData(AuthenticationStatus.Reject, AuthenticationStatus.Awaiting)] - public async Task LoadGroups_NotAcceptedRequest_ShouldSkipGroupLoading(AuthenticationStatus ff, AuthenticationStatus sf) - { - var groupService = new Mock(); - var connectionFactory = new Mock(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("name")]); - - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(replyAttributes); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = ff, SecondFactorStatus = sf }); - - contextMock.SetupProperty(x => x.UserGroups); - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.Empty(context.UserGroups); - } - - [Fact] - public async Task LoadGroups_NoDomainUser_ShouldSkipGroupLoad() - { - var groupService = new Mock(); - var connectionFactory = new Mock(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("memberOf")]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.IsDomainAccount).Returns(false); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns("user"); - contextMock.Setup(x=> x.RequestPacket.AccountType).Returns(AccountType.Local); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - contextMock.SetupProperty(x => x.UserGroups); - - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.Empty(context.UserGroups); - groupService.Verify(x=> x.LoadUserGroups(It.IsAny()), Times.Never); - } - - [Theory] - [InlineData("dc=group1, dc=domain")] - [InlineData("dc=group1, dc=domain;dc=group2, dc=domain")] - [InlineData("dc=group1, dc=domain;dc=group2, dc=domain;dc=group3, dc=domain")] - public async Task LoadGroups_NestedGroupsNotRequired_ShouldGetMemberOfValues(string groups) - { - var groupService = new Mock(); - var connectionFactory = new Mock(); - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("name", "UserGroup=group1")]); - - var memberOf = groups.Split(';').Select(x => new DistinguishedName(x)).ToList(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns(memberOf); - contextMock.SetupProperty(x => x.UserGroups); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(false); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.NotEmpty(context.UserGroups); - Assert.True(memberOf.Select(x => x.Components.Deepest.Value).SequenceEqual(context.UserGroups)); - } - - [Theory] - [InlineData("dc=group1, dc=domain")] - [InlineData("dc=group1, dc=domain;dc=group2, dc=domain")] - [InlineData("dc=group1, dc=domain;dc=group2, dc=domain;dc=group3, dc=domain")] - public async Task LoadGroups_NestedGroupsRequired_ShouldGetMemberOfValues(string groups) - { - var groupService = new Mock(); - groupService.Setup(x => x.LoadUserGroups(It.IsAny())).Returns([]); - - var connectionFactory = new Mock(); - connectionFactory.Setup(x => x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("name", "UserGroup=group1")]); - - var memberOf = groups.Split(';').Select(x => new DistinguishedName(x)).ToList(); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns(memberOf); - contextMock.SetupProperty(x => x.UserGroups); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("dc=group1, dc=domain")); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("Server=localhost;Port=5432"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.NotEmpty(context.UserGroups); - Assert.True(memberOf.Select(x => x.Components.Deepest.Value).SequenceEqual(context.UserGroups)); - } - - [Theory] - [InlineData("group1")] - [InlineData("group1;group2")] - [InlineData("group1;group2;group3")] - public async Task LoadGroups_GroupsFromRoot_ShouldGetUserGroups(string groups) - { - var expectedGroups = groups.Split(';').ToList(); - var groupService = new Mock(); - groupService.Setup(x => x.LoadUserGroups(It.IsAny())).Returns(expectedGroups); - - var connectionFactory = new Mock(); - connectionFactory.Setup(x => x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("name", "UserGroup=group1")]); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([]); - contextMock.SetupProperty(x => x.UserGroups); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("dc=group1, dc=domain")); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("Server=localhost;Port=5432"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.NotEmpty(context.UserGroups); - Assert.True(expectedGroups.SequenceEqual(context.UserGroups)); - } - - [Theory] - [InlineData("group1")] - [InlineData("group1;group2")] - [InlineData("group1;group2;group3")] - public async Task LoadGroups_GroupsFromContainers_ShouldGetUserGroups(string groups) - { - var expectedGroups = groups.Split(';').ToList(); - var groupService = new Mock(); - groupService.Setup(x => x.LoadUserGroups(It.IsAny())).Returns(expectedGroups); - - var connectionFactory = new Mock(); - connectionFactory.Setup(x => x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var attributes = new Dictionary(); - attributes.Add("key", [new RadiusReplyAttributeValue("name", "UserGroup=group1")]); - - var step = new UserGroupLoadingStep(groupService.Object, connectionFactory.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(attributes); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([]); - contextMock.SetupProperty(x => x.UserGroups); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([new DistinguishedName("dc=nested,dc=group")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("dc=group1, dc=domain")); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("Server=localhost;Port=5432"); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.IsDomainAccount).Returns(true); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState() - { FirstFactorStatus = AuthenticationStatus.Accept, SecondFactorStatus = AuthenticationStatus.Accept }); - - var context = contextMock.Object; - context.UserGroups = new(); - - await step.ExecuteAsync(context); - - Assert.NotEmpty(context.UserGroups); - Assert.True(expectedGroups.SequenceEqual(context.UserGroups)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs deleted file mode 100644 index 0cebc535..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius; - -public class NasIdentifierParserTests -{ - [Fact] - public void ParseNasIdentifier_ShouldParse() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - packet.AddAttributeValue("NAS-Identifier", "Test-NAS-Identifier"); - - var bytes = packetService.GetBytes(packet, secret); - RadiusPacketNasIdentifierParser.TryParse(bytes, out var result); - Assert.Equal("Test-NAS-Identifier", result); - } - - [Fact] - public void ParseNasIdentifier_NoAttribute_ShouldReturnNull() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var bytes = packetService.GetBytes(packet, secret); - RadiusPacketNasIdentifierParser.TryParse(bytes, out var result); - Assert.Null(result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs deleted file mode 100644 index 0513106e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; - -public class AttributeReadingTests -{ - [Fact] - public void ReadCustomOctetAttribute_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - packet.AddAttributeValue("State", "TestState"); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - Assert.Equal("TestState", packet.GetAttributeValueAsString("State")); - } - - [Fact] - public void ReadCustomTaggedStringAttribute_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - packet.AddAttributeValue("Tunnel-Client-Auth-ID", "Test-Tunnel-Client-Auth-ID"); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - Assert.Equal("Test-Tunnel-Client-Auth-ID", packet.GetAttribute("Tunnel-Client-Auth-ID")); - Assert.Equal("Test-Tunnel-Client-Auth-ID", packet.GetAttributeValueAsString("Tunnel-Client-Auth-ID")); - } - - [Fact] - public void ReadCustomIntegerAttribute_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - packet.AddAttributeValue("NAS-Port", 123456); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - Assert.Equal(123456, packet.GetAttribute("NAS-Port")); - } - - [Fact] - public void ReadCustomTaggedIntegerAttribute_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - packet.AddAttributeValue("Tunnel-Preference", 123456); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - Assert.Equal(123456, packet.GetAttribute("Tunnel-Preference")); - } - - [Fact] - public void ReadCustomIpAddrAttribute_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - var ipAddr = IPAddress.Parse("127.0.0.1"); - packet.AddAttributeValue("NAS-IP-Address", ipAddr); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - Assert.Equal(ipAddr, packet.GetAttribute("NAS-IP-Address")); - } - - [Fact] - public void ReadCustomIpAddrAttribute_TwoValues_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var ipAddr1 = IPAddress.Parse("127.0.0.1"); - var ipAddr2 = IPAddress.Parse("127.0.0.2"); - packet.AddAttributeValue("NAS-IP-Address", ipAddr1); - packet.AddAttributeValue("NAS-IP-Address", ipAddr2); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - var attributes = packet.GetAttributes("NAS-IP-Address"); - Assert.Collection( - attributes, - e => Assert.Equal(ipAddr1, e), - e => Assert.Equal(ipAddr2, e)); - } - - [Fact] - public void ReadCustomStringAttribute_TwoValues_ShouldRead() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var str1 = "Test-1-Tunnel-Client-Auth-ID"; - var str2 = "Test-2-Tunnel-Client-Auth-ID"; - packet.AddAttributeValue("Tunnel-Client-Auth-ID", str1); - packet.AddAttributeValue("Tunnel-Client-Auth-ID", str2); - - var bytes = packetService.GetBytes(packet, secret); - Assert.NotEmpty(bytes); - - packet = packetService.Parse(bytes, secret); - var attributes = packet.GetAttributes("Tunnel-Client-Auth-ID"); - Assert.Collection( - attributes, - e => Assert.Equal(str1, e), - e => Assert.Equal(str2, e)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs deleted file mode 100644 index 3e419dd8..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; - -public class RadiusPacketParsingTests -{ - [Fact] - public void ParseAccessRequestPacket_ShouldParse() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - Assert.NotNull(packet); - Assert.Equal(PacketCode.AccessRequest, packet.Code); - Assert.Equal("TestUser", packet.GetAttributeValueAsString("User-Name")); - Assert.Equal("TestPassword", packet.GetAttributeValueAsString("User-Password")); - } - - [Fact] - public void ParseStatusServerPacket_ShouldParse() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultStatusServer, secret); - Assert.NotNull(packet); - Assert.Equal(PacketCode.StatusServer, packet.Code); - } - - [Fact] - public void ParseAccessRequestPacket_WrongSharedSecret_PasswordDoesNotMatch() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("999"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - Assert.NotNull(packet); - Assert.NotEqual("TestPassword", packet.GetAttributeValueAsString("User-Password")); - } - - [Fact] - public void SerializeStatusServerPacket_ShouldSerialize() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultStatusServer, secret); - - var packetBytes = packetService.GetBytes(packet, secret); - Assert.True(PacketExamples.DefaultStatusServer.SequenceEqual(packetBytes)); - } - - [Fact] - public void SerializeAccessRequestPacket_ShouldSerialize() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var packetBytes = packetService.GetBytes(packet, secret); - Assert.True(PacketExamples.DefaultAccessRequest.SequenceEqual(packetBytes)); - } - - [Fact] - public void SerializeAccessRequestPacket_WrongSecret_ShouldNotMatch() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("999"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var packetBytes = packetService.GetBytes(packet, secret); - Assert.False(PacketExamples.DefaultAccessRequest.SequenceEqual(packetBytes)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs deleted file mode 100644 index 9ff3eeb7..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; - -public class ResponsePacketTests -{ - [Fact] - public void CreateResponsePacket_ShouldCreateResponsePacket() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var responsePacket = packetService.CreateResponsePacket(packet, PacketCode.AccountingRequest); - - Assert.NotNull(responsePacket); - Assert.Equal(PacketCode.AccountingRequest, responsePacket.Code); - Assert.NotNull(responsePacket.RequestAuthenticator); - Assert.True(packet.Authenticator.Value.SequenceEqual(responsePacket.RequestAuthenticator.Value)); - Assert.Equal(packet.Identifier, responsePacket.Identifier); - } - - [Fact] - public void SerializeResponsePacket_ShouldSerializeResponsePacket() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var responsePacket = packetService.CreateResponsePacket(packet, PacketCode.AccountingRequest); - var responsePacketBytes = packetService.GetBytes(responsePacket, secret); - Assert.NotNull(responsePacketBytes); - - var deserialized = packetService.Parse(responsePacketBytes, secret); - Assert.NotNull(deserialized); - - Assert.Equal(responsePacket.Identifier, deserialized.Identifier); - Assert.Equal(PacketCode.AccountingRequest, responsePacket.Code); - } - - [Fact] - public void SerializeResponsePacket_HasCustomAttributes_ShouldSerializeResponsePacket() - { - var dictionary = TestUtils.GetRadiusDictionary(); - var packetService = new RadiusPacketService(NullLogger.Instance, dictionary); - var secret = new SharedSecret("888"); - var packet = packetService.Parse(PacketExamples.DefaultAccessRequest, secret); - - var responsePacket = packetService.CreateResponsePacket(packet, PacketCode.AccountingRequest); - - var ipAddr = IPAddress.Parse("127.0.0.1"); - responsePacket.AddAttributeValue("State", "TestState"); - responsePacket.AddAttributeValue("NAS-IP-Address", ipAddr); - - var responsePacketBytes = packetService.GetBytes(responsePacket, secret); - Assert.NotNull(responsePacketBytes); - - var deserialized = packetService.Parse(responsePacketBytes, secret); - Assert.NotNull(deserialized); - - Assert.Equal(ipAddr, deserialized.GetAttribute("NAS-IP-Address")); - Assert.Equal("TestState", deserialized.GetAttributeValueAsString("State")); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs deleted file mode 100644 index b371c6ff..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius; - -public class RadiusAttributeTests -{ - [Fact] - public void CreateDefaultRadiusAttribute_ShouldCreate() - { - var attribute = new RadiusAttribute("name"); - Assert.Equal("name", attribute.Name); - Assert.Empty(attribute.Values); - } - - [Fact] - public void AddAttributeValue_NoValue_ShouldThrow() - { - var attribute = new RadiusAttribute("name"); - - Assert.Throws(() => attribute.AddValues()); - Assert.Empty(attribute.Values); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void AddAttributeValue_EmptyValue_ShouldAdd(object value) - { - var attribute = new RadiusAttribute("name"); - - attribute.AddValues(value); - Assert.Single(attribute.Values); - var val = attribute.Values[0]; - Assert.Equal(value, val); - } - - [Fact] - public void AddAttributeValue_ShouldAdd() - { - var attribute = new RadiusAttribute("name"); - var value = "value"; - attribute.AddValues(value); - Assert.Single(attribute.Values); - Assert.Equal(value, attribute.Values[0]); - } - - [Fact] - public void AddAttributeValues_ShouldAddTwoValues() - { - var attribute = new RadiusAttribute("name"); - var value1 = "value1"; - var value2 = "value2"; - - attribute.AddValues(value1); - attribute.AddValues(value2); - Assert.Equal(2, attribute.Values.Count); - Assert.Collection( - attribute.Values, - e => Assert.Equal(value1, e), - e => Assert.Equal(value2, e)); - } - - [Fact] - public void RemoveAttributeValues_ShouldRemove() - { - var attribute = new RadiusAttribute("name"); - var value1 = "value1"; - var value2 = "value2"; - - attribute.AddValues(value1); - attribute.AddValues(value2); - - attribute.RemoveAllValues(); - Assert.Empty(attribute.Values); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs deleted file mode 100644 index 656ad9a3..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Radius; - -public class RadiusPacketTests -{ - [Fact] - public void CreateDefaultRadiusPacket_ShouldCreate() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - Assert.Equal(header.Code, packet.Code); - Assert.Equal(header.Identifier, packet.Identifier); - Assert.Equal(header.Authenticator, packet.Authenticator); - Assert.Equal(requestAuthenticator, packet.RequestAuthenticator); - Assert.Empty(packet.Attributes); - } - - [Fact] - public void AddPacketAttribute_ShouldAddSingleAttribute() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - var attrName = "name"; - var attrValue = "value"; - packet.AddAttributeValue(attrName, attrValue); - Assert.Single(packet.Attributes); - Assert.Contains(attrName, packet.Attributes); - var attribute = packet.Attributes[attrName]; - Assert.NotNull(attribute); - } - - [Fact] - public void AddPacketAttribute_ShouldAddTwoDifferentAttributes() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - var attrName1 = "name1"; - var attrName2 = "name2"; - var attrValue = "value"; - packet.AddAttributeValue(attrName1, attrValue); - packet.AddAttributeValue(attrName2, attrValue); - Assert.Equal(2, packet.Attributes.Count); - Assert.Contains(attrName1, packet.Attributes); - Assert.Contains(attrName2, packet.Attributes); - var attribute = packet.Attributes[attrName1]; - Assert.NotNull(attribute); - attribute = packet.Attributes[attrName2]; - Assert.NotNull(attribute); - } - - [Fact] - public void AddPacketAttribute_ShouldAddSameAttributes() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header,requestAuthenticator); - var attrName = "name1"; - var attrValue = "value"; - packet.AddAttributeValue(attrName, attrValue); - packet.AddAttributeValue(attrName, attrValue); - - Assert.Single(packet.Attributes); - Assert.Contains(attrName, packet.Attributes); - - var attribute = packet.Attributes[attrName]; - Assert.NotNull(attribute); - Assert.Equal(2, attribute.Values.Count); - } - - [Fact] - public void ReplaceAttribute_ShouldReplaceAttribute() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - var attrName = "name1"; - var attrValue = "value"; - packet.AddAttributeValue(attrName, attrValue); - - var attribute = packet.Attributes[attrName]; - Assert.NotNull(attribute); - Assert.Contains(attrValue, attribute.Values); - - var newValue = "newValue"; - packet.ReplaceAttribute(attrName, newValue); - Assert.Single(packet.Attributes); - - attribute = packet.Attributes[attrName]; - Assert.NotNull(attribute); - Assert.Contains(newValue, attribute.Values); - Assert.DoesNotContain(attrValue, attribute.Values); - } - - [Fact] - public void RemoveAttribute_ShouldRemoveAttribute() - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - var attrName = "name1"; - var attrValue = "value"; - - packet.AddAttributeValue(attrName, attrValue); - Assert.Single(packet.Attributes); - - packet.RemoveAttribute(attrName); - Assert.DoesNotContain(attrName, packet.Attributes); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void AddAttributeValue_EmptyName_ShouldThrow(string emptyString) - { - var header = RadiusPacketHeader.Create(PacketCode.AccessRequest, 1); - var requestAuthenticator = new RadiusAuthenticator(); - var packet = new RadiusPacket(header, requestAuthenticator); - var attrName = emptyString; - var attrValue = "value"; - - Assert.Throws(() => packet.AddAttributeValue(attrName, attrValue)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs deleted file mode 100644 index 5fd7644c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Server; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.PipelineTests; - -namespace Multifactor.Radius.Adapter.v2.Tests.Server; - -public class UdpPacketHandlerTests -{ - [Theory] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - [InlineData(15)] - [InlineData(30)] - [InlineData(30000)] - [InlineData(60000)] - public async Task MultipleRequests_ShouldProcess(int connectionsCount) - { - var configMock = new Mock(); - var clientConfigMock = new Mock(); - clientConfigMock.Setup(x => x.RadiusSharedSecret).Returns("secret"); - var pipelineProviderMock = new Mock(); - var pipelineMock = new PipelineMock(); - pipelineProviderMock.Setup(x => x.GetRadiusPipeline(It.IsAny())).Returns(pipelineMock); - var packetServiceMock = new Mock(); - packetServiceMock - .Setup(x => x.Parse(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(() => new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte [16]))); - var nas = "nas"; - packetServiceMock.Setup(x => x.TryGetNasIdentifier(It.IsAny(), out nas)).Returns(true); - configMock.Setup(x => x.GetClient(It.IsAny())).Returns(clientConfigMock.Object); - - var cache = new Mock(); - var outVal = new object(); - cache.Setup(x => x.TryGetValue(It.IsAny(), out outVal)).Returns(false); - - var handler = new UdpPacketHandler(configMock.Object, packetServiceMock.Object, cache.Object, new Mock().Object ,NullLogger.Instance); - var tasks = new List(); - - for(int i = 0; i < connectionsCount; i++) - { - var task = Task.Factory.StartNew(() => handler.HandleUdpPacket(new UdpReceiveResult(new byte[0], IPEndPoint.Parse("127.0.0.1:1812"))), TaskCreationOptions.LongRunning); - tasks.Add(task); - } - - await Task.WhenAll(tasks); - foreach (var t in tasks) - { - Assert.True(t.IsCompletedSuccessfully); - } - } - - private class PipelineMock : IRadiusPipeline - { - private Random _random = new(); - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - var delay = _random.Next(1, 15) * 1000; - await Task.Delay(delay); - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs deleted file mode 100644 index bcc64d7d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Extensions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Tests.ServiceCollectionExtensionsTests; - -public class AddPipelineTests -{ - [Fact] - public void AddPipelineSteps_ShouldAddPipeline() - { - var pipelineKey = "MyPipeline"; - var host = Host.CreateApplicationBuilder(); - host.Services.AddSingleton(new ApplicationVariables()); - var configuration = new PipelineConfiguration([typeof(StatusServerFilteringStep), typeof(AccessRequestFilteringStep)]); - host.Services.AddPipeline(pipelineKey, configuration); - var app = host.Build(); - var pipeline = app.Services.GetKeyedService(pipelineKey); - Assert.NotNull(pipeline); - } - - [Fact] - public void AddPipeline_ShouldAddTwoPipelines() - { - var pipelineKey1 = "1"; - var pipelineKey2 = "2"; - - var host = Host.CreateApplicationBuilder(); - - var configuration1 = new PipelineConfiguration([typeof(StatusServerFilteringStep), typeof(AccessRequestFilteringStep)]); - var configuration2 = new PipelineConfiguration([typeof(AccessRequestFilteringStep), typeof(AccessRequestFilteringStep)]); - host.Services.AddSingleton(new ApplicationVariables()); - host.Services.AddPipeline(pipelineKey1, configuration1); - host.Services.AddPipeline(pipelineKey2, configuration2); - var app = host.Build(); - - var pipeline1 = app.Services.GetKeyedService(pipelineKey1); - var pipeline2 = app.Services.GetKeyedService(pipelineKey2); - - Assert.NotNull(pipeline1); - Assert.NotNull(pipeline2); - } - - [Fact] - public void NoPipelineRegistry_ShouldReturnNull() - { - var pipelineKey1 = "1"; - var host = Host.CreateApplicationBuilder(); - var app = host.Build(); - var pipeline = app.Services.GetKeyedService(pipelineKey1); - Assert.Null(pipeline); - } - - [Fact] - public void StepTypeDoesNotImplementIPipelineInterface_ShouldThrow() - { - var pipelineKey = "MyPipeline"; - var host = Host.CreateApplicationBuilder(); - var stepsTypes = new PipelineConfiguration([typeof(XmlAppConfigurationSource)]); - Assert.Throws(() => host.Services.AddPipeline(pipelineKey, stepsTypes)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironment.cs b/src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironment.cs deleted file mode 100644 index 1c40f721..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironment.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Tests; - -internal enum TestAssetLocation -{ - RootDirectory, - ClientsDirectory, - SensitiveData -} - -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.SensitiveData => $"{_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.v2.Tests/TestEnvironmentVariables.cs b/src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironmentVariables.cs deleted file mode 100644 index 752890c7..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironmentVariables.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Tests; - -/// -/// 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 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; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs deleted file mode 100644 index 84059ab2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs +++ /dev/null @@ -1,424 +0,0 @@ -# nullable disable -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit; - -public class AdapterResponseSenderTests -{ - [Fact] - public async Task SendResponse_ShouldSkipResponse() - { - var contextMock = new Mock(); - contextMock.Setup(x => x.ResponsePacket).Returns(() => null); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(true); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState()); - contextMock.Setup(x => x.ResponseInformation).Returns(new ResponseInformation()); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - - var packetServiceMock = new Mock(); - var attributeServiceMock = new Mock(); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(contextMock.Object); - - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task SendResponse_EapMessageChallenge_ShouldSendResponse() - { - var contextMock = new Mock(); - - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket.IsEapMessageChallenge).Returns(true); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState()); - contextMock.Setup(x => x.ResponseInformation).Returns(new ResponseInformation()); - - var packetServiceMock = new Mock(); - var attributeServiceMock = new Mock(); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var responsePacketMock = new Mock(); - responsePacketMock.Setup(x => x.IsEapMessageChallenge).Returns(true); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x=> x.Identifier).Returns(1); - - var request = new SendAdapterResponseRequest(contextMock.Object); - - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_VendorAclRequest_ShouldSendResponse() - { - var contextMock = new Mock(); - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(new Mock().Object); - contextMock.Setup(x => x.ResponsePacket.IsEapMessageChallenge).Returns(false); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(true); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.AuthenticationState).Returns(new AuthenticationState()); - contextMock.Setup(x => x.ResponseInformation).Returns(new ResponseInformation()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - var packetServiceMock = new Mock(); - var attributeServiceMock = new Mock(); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(contextMock.Object); - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_AccessAcceptNoResponsePacket_ShouldSendResponse() - { - var contextMock = new Mock(); - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(() => null); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessAccept)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_AccessAcceptHasResponsePacket_ShouldSendResponse() - { - var contextMock = new Mock(); - var responsePacketMock = new Mock(); - responsePacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(responsePacketMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessAccept)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_AccessRejectHasResponsePacket_ShouldSendResponse() - { - var contextMock = new Mock(); - var responsePacketMock = new Mock(); - responsePacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - responsePacketMock.Setup(x => x.Code).Returns(PacketCode.AccessReject); - - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(responsePacketMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - contextMock.Setup(x => x.InvalidCredentialDelay).Returns(RandomWaiterConfig.Create("0-0")); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessReject, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessReject)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - - var udpClientMock = new Mock(); - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_AccessRejectNoResponsePacket_ShouldSendResponse() - { - var contextMock = new Mock(); - - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(() => null); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - contextMock.Setup(x => x.InvalidCredentialDelay).Returns(RandomWaiterConfig.Create("0-0")); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessReject, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessReject)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - - var udpClientMock = new Mock(); - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendResponse_AccessAccept_ShouldAddResponseAttributes() - { - var contextMock = new Mock(); - var responsePacketMock = new Mock(); - var attribute = new RadiusAttribute("key"); - attribute.AddValues("customValue"); - responsePacketMock.Setup(x => x.Attributes.Values).Returns([attribute]); - - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(responsePacketMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessAccept)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - Assert.True(packet.Attributes.ContainsKey("key")); - } - - [Fact] - public async Task SendResponse_AccessAccept_ShouldAddReplyAttributes() - { - var contextMock = new Mock(); - var responsePacketMock = new Mock(); - responsePacketMock.Setup(x => x.Attributes.Values).Returns([]); - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(responsePacketMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessAccept)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - - var attributeServiceMock = new Mock(); - var replyAttributes = new Dictionary> { { "key", new List() { 123 } } }; - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(replyAttributes); - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - Assert.True(packet.Attributes.ContainsKey("key")); - } - - [Fact] - public async Task SendResponse_AccessRejectHasResponsePacket_ShouldAddResponseAttributes() - { - var contextMock = new Mock(); - var responsePacketMock = new Mock(); - var attribute = new RadiusAttribute("key"); - attribute.AddValues("customValue"); - responsePacketMock.Setup(x => x.Attributes.Values).Returns([attribute]); - responsePacketMock.Setup(x => x.Code).Returns(PacketCode.AccessReject); - - contextMock.Setup(x => x.ExecutionState.ShouldSkipResponse).Returns(false); - contextMock.Setup(x => x.ResponsePacket).Returns(responsePacketMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.Identifier).Returns(1); - contextMock.Setup(x => x.RequestPacket.IsVendorAclRequest).Returns(false); - contextMock.SetupProperty(x => x.AuthenticationState); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x => x.RequestPacket.Attributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserGroups).Returns([]); - contextMock.Setup(x => x.RadiusReplyAttributes).Returns(new Dictionary()); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ResponseInformation.ReplyMessage).Returns("replyMessage"); - contextMock.Setup(x => x.ResponseInformation.State).Returns("state"); - contextMock.Setup(x => x.InvalidCredentialDelay).Returns(RandomWaiterConfig.Create("0-0")); - var context = contextMock.Object; - context.AuthenticationState = new AuthenticationState(); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - - var packetServiceMock = new Mock(); - var packet = new RadiusPacket(new RadiusPacketHeader(PacketCode.AccessReject, 1, new byte[16])); - var packetBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - packetServiceMock.Setup(x => x.CreateResponsePacket(It.IsAny(), PacketCode.AccessReject)).Returns(packet); - packetServiceMock.Setup(x => x.GetBytes(packet, It.IsAny())).Returns(packetBytes); - var attributeServiceMock = new Mock(); - attributeServiceMock.Setup(x => x.GetReplyAttributes(It.IsAny())).Returns(new Dictionary>()); - - var udpClientMock = new Mock(); - - var sender = new AdapterResponseSender(packetServiceMock.Object, udpClientMock.Object, attributeServiceMock.Object, NullLogger.Instance); - var request = new SendAdapterResponseRequest(context); - await sender.SendResponse(request); - - udpClientMock.Verify(x => x.SendAsync(packetBytes, It.IsAny(), It.IsAny()), Times.Once); - Assert.True(packet.Attributes.ContainsKey("key")); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs deleted file mode 100644 index 7e50abb5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.DirectoryServices.Protocols; -using System.Net; -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.FirstFactorAuth; - -public class LdapFirstFactorProcessorTests -{ - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) - { - //Arrange - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(login); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) - { - //Arrange - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("correctLogin"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(pwd); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Fact] - public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() - { - //Arrange - var factoryMock = new Mock(); - factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); - factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); - var factory = new CustomLdapConnectionFactory([factoryMock.Object]); - var formatterProviderMock = new Mock(); - var processor = new LdapFirstFactorProcessor(factory, formatterProviderMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("user"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.SetupProperty(x => x.MustChangePasswordDomain); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.LdapSchema.LdapServerImplementation).Returns(LdapImplementation.ActiveDirectory); - var context = contextMock.Object; - - //Act - await processor.ProcessFirstFactor(context); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal("your.domain", context.MustChangePasswordDomain); - } - - private LdapException GetLdapException() - { - var ex = new LdapException(1, "message", "data 773"); - return ex; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs deleted file mode 100644 index 735fdacc..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.FirstFactorAuthTests; - -public class RadiusFirstFactorProcessorTests -{ - [Fact] - public async Task ProcessFirstFactor_ShouldAccept() - { - //Arrange - var clientFactoryMock = new Mock(); - var clientMock = new Mock(); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync([]); - clientFactoryMock.Setup(x => x.CreateRadiusClient(It.IsAny())).Returns(clientMock.Object); - - var packetService = new Mock(); - packetService.Setup(x => x.GetBytes(It.IsAny(),It.IsAny())).Returns([]); - - var responseMock = new Mock(); - responseMock.Setup(x => x.Code).Returns(PacketCode.AccessAccept); - - packetService.Setup(x => x.Parse(It.IsAny(), It.IsAny(), It.IsAny())).Returns(responseMock.Object); - var processor = new RadiusFirstFactorProcessor(packetService.Object, clientFactoryMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x => x.UserName).Returns("username"); - requestPacketMock.Setup(x => x.Code).Returns(PacketCode.AccessRequest); - requestPacketMock.Setup(x => x.Identifier).Returns(1); - requestPacketMock.Setup(x => x.Authenticator).Returns(new RadiusAuthenticator()); - requestPacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.RequestPacket).Returns(requestPacketMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse("127.0.0.1")])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); - } - - [Theory] - [InlineData(PacketCode.StatusServer)] - [InlineData(PacketCode.AccessReject)] - [InlineData(PacketCode.AccessChallenge)] - public async Task ProcessFirstFactor_NoneAcceptCode_ShouldReturnReject(PacketCode responseCode) - { - //Arrange - var clientFactoryMock = new Mock(); - var clientMock = new Mock(); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync([]); - clientFactoryMock.Setup(x => x.CreateRadiusClient(It.IsAny())).Returns(clientMock.Object); - - var packetService = new Mock(); - packetService.Setup(x => x.GetBytes(It.IsAny(),It.IsAny())).Returns([]); - - var responseMock = new Mock(); - responseMock.Setup(x => x.Code).Returns(responseCode); - - packetService.Setup(x => x.Parse(It.IsAny(), It.IsAny(), It.IsAny())).Returns(responseMock.Object); - var processor = new RadiusFirstFactorProcessor(packetService.Object, clientFactoryMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x => x.UserName).Returns("username"); - requestPacketMock.Setup(x => x.Code).Returns(PacketCode.AccessRequest); - requestPacketMock.Setup(x => x.Identifier).Returns(1); - requestPacketMock.Setup(x => x.Authenticator).Returns(new RadiusAuthenticator()); - requestPacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.RequestPacket).Returns(requestPacketMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse("127.0.0.1")])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessFirstFactor_ResponseIsNull_ShouldReject() - { - //Arrange - var clientFactoryMock = new Mock(); - var clientMock = new Mock(); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(() => null); - clientFactoryMock.Setup(x => x.CreateRadiusClient(It.IsAny())).Returns(clientMock.Object); - - var packetService = new Mock(); - packetService.Setup(x => x.GetBytes(It.IsAny(),It.IsAny())).Returns([]); - var processor = new RadiusFirstFactorProcessor(packetService.Object, clientFactoryMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x => x.UserName).Returns("username"); - requestPacketMock.Setup(x => x.Code).Returns(PacketCode.AccessRequest); - requestPacketMock.Setup(x => x.Identifier).Returns(1); - requestPacketMock.Setup(x => x.Authenticator).Returns(new RadiusAuthenticator()); - requestPacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.RequestPacket).Returns(requestPacketMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse("127.0.0.1")])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Fact] - public async Task ProcessFirstFactor_MultipleNpsServersAndResponseIsNull_ShouldReject() - { - //Arrange - var clientFactoryMock = new Mock(); - var clientMock = new Mock(); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(() => null); - clientFactoryMock.Setup(x => x.CreateRadiusClient(It.IsAny())).Returns(clientMock.Object); - - var packetService = new Mock(); - packetService.Setup(x => x.GetBytes(It.IsAny(),It.IsAny())).Returns([]); - var processor = new RadiusFirstFactorProcessor(packetService.Object, clientFactoryMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x => x.UserName).Returns("username"); - requestPacketMock.Setup(x => x.Code).Returns(PacketCode.AccessRequest); - requestPacketMock.Setup(x => x.Identifier).Returns(1); - requestPacketMock.Setup(x => x.Authenticator).Returns(new RadiusAuthenticator()); - requestPacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.RequestPacket).Returns(requestPacketMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([IPEndPoint.Parse("127.0.0.1"), IPEndPoint.Parse("127.0.0.2")])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - clientMock.Verify(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - } - - [Fact] - public async Task ProcessFirstFactor_MultipleNpsServersAndAcceptCode_ShouldAccept() - { - var nps1 = IPEndPoint.Parse("127.0.0.1"); - var nps2 = IPEndPoint.Parse("127.0.0.2"); - //Arrange - var clientFactoryMock = new Mock(); - var clientMock = new Mock(); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.Is(i => i == nps1), It.IsAny())).ReturnsAsync(() => null); - clientMock.Setup(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.Is(i => i == nps2), It.IsAny())).ReturnsAsync(() => []); - clientFactoryMock.Setup(x => x.CreateRadiusClient(It.IsAny())).Returns(clientMock.Object); - - var packetService = new Mock(); - packetService.Setup(x => x.GetBytes(It.IsAny(),It.IsAny())).Returns([]); - var responseMock = new Mock(); - responseMock.Setup(x => x.Code).Returns(PacketCode.AccessAccept); - packetService.Setup(x => x.Parse(It.IsAny(), It.IsAny(), It.IsAny())).Returns(responseMock.Object); - - var processor = new RadiusFirstFactorProcessor(packetService.Object, clientFactoryMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - var requestPacketMock = new Mock(); - requestPacketMock.Setup(x => x.UserName).Returns("username"); - requestPacketMock.Setup(x => x.Code).Returns(PacketCode.AccessRequest); - requestPacketMock.Setup(x => x.Identifier).Returns(1); - requestPacketMock.Setup(x => x.Authenticator).Returns(new RadiusAuthenticator()); - requestPacketMock.Setup(x => x.Attributes).Returns(new Dictionary()); - - contextMock.Setup(x => x.RequestPacket).Returns(requestPacketMock.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.NpsServerEndpoints).Returns(new HashSet([nps1, nps2])); - contextMock.Setup(x => x.ServiceClientEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.RadiusSharedSecret).Returns(new SharedSecret("123")); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - - //Act - await processor.ProcessFirstFactor(contextMock.Object); - - //Assert - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); - clientMock.Verify(x => x.SendPacketAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs deleted file mode 100644 index d8de82f8..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class ActiveDirectoryTests -{ - [Fact] - public void FormatName_ShouldReturnSameName() - { - //Arrange - var formatter = new ActiveDirectoryFormatter(); - - var profileMock = new Mock(); - var name = "userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs deleted file mode 100644 index 47e41809..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class FreeIpaTests -{ - [Fact] - public void FormatName_Uid_ShouldReturnDn() - { - //Arrange - var formatter = new FreeIpaFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_NetBiosName_ShouldReturnDn() - { - //Arrange - var formatter = new FreeIpaFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "domain\\userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_Dn_ShouldReturnDn() - { - //Arrange - var formatter = new FreeIpaFormatter(); - var profileMock = new Mock(); - var name = "dc=domain,dc=com"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } - - [Fact] - public void FormatName_Upn_ShouldReturnUpn() - { - //Arrange - var formatter = new FreeIpaFormatter(); - var profileMock = new Mock(); - var name = "user@domain"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs deleted file mode 100644 index 6d4a3dbe..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class LdapBindNameFormatterProviderTests -{ - [Theory] - [InlineData(LdapImplementation.ActiveDirectory)] - [InlineData(LdapImplementation.OpenLDAP)] - [InlineData(LdapImplementation.Samba)] - [InlineData(LdapImplementation.FreeIPA)] - [InlineData(LdapImplementation.MultiDirectory)] - public void GetLdapBindNameFormatter_ShouldReturnRequiredFormatter(LdapImplementation ldapImplementation) - { - //Arrange - var processor = new Mock(); - processor.Setup(x => x.LdapImplementation).Returns(ldapImplementation); - var provider = new LdapBindNameFormatterProvider([processor.Object]); - - //Act - var formatter = provider.GetLdapBindNameFormatter(ldapImplementation); - - //Assert - Assert.NotNull(formatter); - Assert.Equal(ldapImplementation, formatter.LdapImplementation); - } - - [Theory] - [InlineData(LdapImplementation.ActiveDirectory)] - [InlineData(LdapImplementation.OpenLDAP)] - [InlineData(LdapImplementation.Samba)] - [InlineData(LdapImplementation.FreeIPA)] - [InlineData(LdapImplementation.MultiDirectory)] - public void GetLdapBindNameFormatter_NoSuchFormatter_ShouldReturnNull(LdapImplementation ldapImplementation) - { - //Arrange - var processor = new Mock(); - processor.Setup(x => x.LdapImplementation).Returns(LdapImplementation.Unknown); - var provider = new LdapBindNameFormatterProvider([processor.Object]); - - //Act - var formatter = provider.GetLdapBindNameFormatter(ldapImplementation); - - //Assert - Assert.Null(formatter); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs deleted file mode 100644 index 4b559c43..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class MultiDirectoryTests -{ - [Fact] - public void FormatName_Uid_ShouldReturnDn() - { - //Arrange - var formatter = new MultiDirectoryFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_NetBiosName_ShouldReturnDn() - { - //Arrange - var formatter = new MultiDirectoryFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "domain\\userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_Dn_ShouldReturnDn() - { - //Arrange - var formatter = new MultiDirectoryFormatter(); - var profileMock = new Mock(); - var name = "dc=domain,dc=com"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } - - [Fact] - public void FormatName_Upn_ShouldReturnUpn() - { - //Arrange - var formatter = new MultiDirectoryFormatter(); - var profileMock = new Mock(); - var name = "user@domain"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs deleted file mode 100644 index d0a1c3bc..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class OpenLdapTests -{ - [Fact] - public void FormatName_Uid_ShouldReturnDn() - { - //Arrange - var formatter = new OpenLdapFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_NetBiosName_ShouldReturnDn() - { - //Arrange - var formatter = new OpenLdapFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "domain\\userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_Dn_ShouldReturnDn() - { - //Arrange - var formatter = new OpenLdapFormatter(); - var profileMock = new Mock(); - var name = "dc=domain,dc=com"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } - - [Fact] - public void FormatName_Upn_ShouldReturnUpn() - { - //Arrange - var formatter = new OpenLdapFormatter(); - var profileMock = new Mock(); - var name = "user@domain"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs deleted file mode 100644 index 8cfa352c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; - -public class SambaTests -{ - [Fact] - public void FormatName_Uid_ShouldReturnDn() - { - //Arrange - var formatter = new SambaFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_NetBiosName_ShouldReturnDn() - { - //Arrange - var formatter = new SambaFormatter(); - var profileMock = new Mock(); - var dn = new DistinguishedName("cn=user,dc=domain,dc=com"); - profileMock.Setup(x => x.Dn).Returns(dn); - var name = "domain\\userName"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(dn.StringRepresentation, result); - } - - [Fact] - public void FormatName_Dn_ShouldReturnDn() - { - //Arrange - var formatter = new SambaFormatter(); - var profileMock = new Mock(); - var name = "dc=domain,dc=com"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } - - [Fact] - public void FormatName_Upn_ShouldReturnUpn() - { - //Arrange - var formatter = new SambaFormatter(); - var profileMock = new Mock(); - var name = "user@domain"; - - //Act - var result = formatter.FormatName(name, profileMock.Object); - - //Assert - Assert.Equal(name, result); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapForestServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapForestServiceTests.cs deleted file mode 100644 index bae2e08a..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapForestServiceTests.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit; - -public class LdapForestServiceTests -{ - [Fact] - public void LoadLdapForest_EmptyMainSchema_ShouldReturnEmptyForest() - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - ldapSchemaLoaderMock.Setup(x => x.Load(It.IsAny())).Returns(() => null); - var connectionFactoryMock = new Mock(); - var domainsLoaderProviderMock = new Mock(); - var cacheMock = new Mock(); - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, true, true); - - //Assert - Assert.Empty(result); - } - - [Theory] - [InlineData(LdapImplementation.OpenLDAP)] - [InlineData(LdapImplementation.Samba)] - [InlineData(LdapImplementation.Unknown)] - [InlineData(LdapImplementation.ActiveDirectory)] - [InlineData(LdapImplementation.MultiDirectory)] - [InlineData(LdapImplementation.FreeIPA)] - public void LoadLdapForest_NoTrustedDomainsLoader_ShouldReturnMainSchema(LdapImplementation ldapImplementation) - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - var ldapSchemaMock = new Mock(); - var namingContext = new DistinguishedName("dc=domain,dc=com"); - ldapSchemaMock.Setup(x => x.LdapServerImplementation).Returns(ldapImplementation); - ldapSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - - ldapSchemaLoaderMock.Setup(x => x.Load(It.IsAny())).Returns(ldapSchemaMock.Object); - var connectionFactoryMock = new Mock(); - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(ldapImplementation)).Returns(() => null); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, true, true); - - //Assert - Assert.NotNull(result); - Assert.Single(result); - var tree = result.First(); - Assert.Equal(ldapSchemaMock.Object, tree.Schema); - } - - [Fact] - public void LoadLdapForest_LoadTrustedDomainsFalse_ShouldReturnMainSchema() - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - var ldapSchemaMock = new Mock(); - var namingContext = new DistinguishedName("dc=domain,dc=com"); - ldapSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.Unknown); - ldapSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - ldapSchemaLoaderMock.Setup(x => x.Load(It.IsAny())).Returns(ldapSchemaMock.Object); - - var connectionFactoryMock = new Mock(); - var domainLoaderMock = new Mock(); - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(It.IsAny())).Returns(() => domainLoaderMock.Object); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, false, false); - - //Assert - Assert.NotNull(result); - Assert.Single(result); - var tree = result.First(); - Assert.Equal(ldapSchemaMock.Object, tree.Schema); - domainLoaderMock.Verify(x => x.LoadTrustedDomains(It.IsAny(), It.IsAny()), Times.Never()); - ldapSchemaLoaderMock.Verify(x => x.Load(It.IsAny()), Times.Once); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void LoadLdapForest_LoadTrustedDomainsTrue_ShouldReturnMainSchemaAndTrustedDomain(int trustedDomainsCount) - { - //Arrange - var options = GetConnectionOptions(); - var ldapSchemaLoaderMock = new Mock(); - var rootSchemaMock = new Mock(); - - var namingContext = new DistinguishedName("dc=domain,dc=com"); - rootSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.ActiveDirectory); - rootSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - ldapSchemaLoaderMock.Setup(x => x.Load(It.Is(c => c.ConnectionString == options.ConnectionString))).Returns(rootSchemaMock.Object); - - var trustedSchemaMock = new Mock(); - trustedSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.Unknown); - trustedSchemaMock.Setup(x => x.NamingContext).Returns(new DistinguishedName("dc=trusted,dc=domain")); - ldapSchemaLoaderMock.Setup(x => x.Load(It.Is(c => c.ConnectionString != options.ConnectionString))).Returns(trustedSchemaMock.Object); - - var connectionFactoryMock = new Mock(); - var domainLoaderMock = new Mock(); - var trustedDomains = Enumerable.Repeat(new DistinguishedName("dc=trusted,dc=domain"), trustedDomainsCount); - - domainLoaderMock.Setup(x=> x.LoadTrustedDomains(It.IsAny(), rootSchemaMock.Object)).Returns(trustedDomains); - - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(LdapImplementation.ActiveDirectory)).Returns(() => domainLoaderMock.Object); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - - //Act - var result = forestService.LoadLdapForest(options, true, false); - - //Assert - var domainsCount = trustedDomainsCount + 1; - Assert.NotNull(result); - Assert.Equal(domainsCount, result.Count); - domainLoaderMock.Verify(x => x.LoadTrustedDomains(It.IsAny(), It.IsAny()), Times.Once()); - ldapSchemaLoaderMock.Verify(x => x.Load(It.IsAny()), Times.Exactly(domainsCount)); - } - - [Fact] - public void LoadLdapForest_LoadSuffixesFalse_ShouldReturnMainSuffix() - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - var ldapSchemaMock = new Mock(); - var namingContext = new DistinguishedName("dc=domain,dc=com"); - ldapSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.Unknown); - ldapSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - ldapSchemaLoaderMock.Setup(x => x.Load(It.IsAny())).Returns(ldapSchemaMock.Object); - - var connectionFactoryMock = new Mock(); - var domainLoaderMock = new Mock(); - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(It.IsAny())).Returns(() => domainLoaderMock.Object); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, false, false); - - //Assert - Assert.NotNull(result); - Assert.Single(result); - var tree = result.First(); - Assert.Equal(ldapSchemaMock.Object, tree.Schema); - Assert.Single(tree.Suffixes); - domainLoaderMock.Verify(x => x.LoadDomainSuffixes(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void LoadLdapForest_LoadSuffixesTrue_ShouldReturnAllSuffixes(int suffixCount) - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - var ldapSchemaMock = new Mock(); - var namingContext = new DistinguishedName("dc=domain,dc=com"); - ldapSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.Unknown); - ldapSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - ldapSchemaLoaderMock.Setup(x => x.Load(It.IsAny())).Returns(ldapSchemaMock.Object); - - var connectionFactoryMock = new Mock(); - var domainLoaderMock = new Mock(); - var suffixes = new List(suffixCount); - for (var i = 0; i < suffixCount; i++) - suffixes.Add("suffix" + i); - - domainLoaderMock.Setup(x=> x.LoadDomainSuffixes(It.IsAny(), It.IsAny())).Returns(suffixes); - - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(It.IsAny())).Returns(() => domainLoaderMock.Object); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, false, true); - - //Assert - Assert.NotNull(result); - Assert.Single(result); - var tree = result.First(); - Assert.Equal(ldapSchemaMock.Object, tree.Schema); - var count = suffixCount + 1; - Assert.Equal(count, tree.Suffixes.Count); - domainLoaderMock.Verify(x => x.LoadDomainSuffixes(It.IsAny(), It.IsAny()), Times.Once()); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void LoadLdapForest_LoadTrustedDomainsAndLoadSuffixesTrue_ShouldReturnAllDomainsAndSuffixes(int counter) - { - //Arrange - var options = GetConnectionOptions(); - var ldapSchemaLoaderMock = new Mock(); - var rootSchemaMock = new Mock(); - var namingContext = new DistinguishedName("dc=domain,dc=com"); - rootSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.Unknown); - rootSchemaMock.Setup(x => x.NamingContext).Returns(namingContext); - ldapSchemaLoaderMock.Setup(x => x.Load(It.Is(c => c.ConnectionString == options.ConnectionString))).Returns(rootSchemaMock.Object); - - var trustedSchemaMock = new Mock(); - trustedSchemaMock.Setup(x => x.LdapServerImplementation).Returns(LdapImplementation.ActiveDirectory); - trustedSchemaMock.Setup(x => x.NamingContext).Returns(new DistinguishedName("dc=trusted,dc=domain")); - ldapSchemaLoaderMock.Setup(x => x.Load(It.Is(c => c.ConnectionString != options.ConnectionString))).Returns(trustedSchemaMock.Object); - - var connectionFactoryMock = new Mock(); - var domainLoaderMock = new Mock(); - var suffixes = new List(counter); - for (var i = 0; i < counter; i++) - suffixes.Add("suffix" + i); - var trustedDomains = Enumerable.Repeat(new DistinguishedName("dc=trusted,dc=domain"), counter); - domainLoaderMock.Setup(x=> x.LoadTrustedDomains(It.IsAny(), It.Is(s => s == rootSchemaMock.Object))).Returns(trustedDomains); - domainLoaderMock.Setup(x=> x.LoadTrustedDomains(It.IsAny(), It.Is(s => s == trustedSchemaMock.Object))).Returns([]); - domainLoaderMock.Setup(x=> x.LoadDomainSuffixes(It.IsAny(), It.IsAny())).Returns(suffixes); - - var domainsLoaderProviderMock = new Mock(); - domainsLoaderProviderMock.Setup(x => x.GetTrustedDomainsLoader(It.IsAny())).Returns(() => domainLoaderMock.Object); - var cacheMock = new Mock(); - - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - - //Act - var result = forestService.LoadLdapForest(options, true, true); - - //Assert - var expectedEntitiesCount = counter + 1; - Assert.NotNull(result); - Assert.Equal(expectedEntitiesCount, result.Count); - Assert.True(result.All(x => x.Suffixes.Count == expectedEntitiesCount)); - domainLoaderMock.Verify(x => x.LoadDomainSuffixes(It.IsAny(), It.IsAny()), Times.Exactly(expectedEntitiesCount)); - domainLoaderMock.Verify(x => x.LoadTrustedDomains(It.IsAny(), It.IsAny()), Times.Once); - ldapSchemaLoaderMock.Verify(x => x.Load(It.IsAny()), Times.Exactly(expectedEntitiesCount)); - } - - [Fact] - public void LoadLdapForest_ShouldLoadFromCache() - { - //Arrange - var ldapSchemaLoaderMock = new Mock(); - var connectionFactoryMock = new Mock(); - var domainsLoaderProviderMock = new Mock(); - var cacheMock = new Mock(); - var key = "forest_url"; - IReadOnlyCollection forest = new List { new LdapForestEntry(LdapSchemaBuilder.Default) }; - cacheMock.Setup(x => x.TryGetValue(key, out forest)).Returns(true); - var forestService = new LdapForestService( - ldapSchemaLoaderMock.Object, - connectionFactoryMock.Object, - domainsLoaderProviderMock.Object, - cacheMock.Object, - NullLogger.Instance); - var options = GetConnectionOptions(); - - //Act - var result = forestService.LoadLdapForest(options, true, true); - - //Assert - Assert.Single(result); - cacheMock.Verify(x => x.TryGetValue(key, out forest), Times.Once); - } - - private LdapConnectionOptions GetConnectionOptions() => new(new LdapConnectionString("url"), AuthType.Basic, "name", "password"); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs deleted file mode 100644 index 428e7349..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs +++ /dev/null @@ -1,294 +0,0 @@ -# nullable disable -using Moq; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.LdapGroup.Load; -using Multifactor.Core.Ldap.LdapGroup.Membership; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit; - -public class LdapGroupServiceTests -{ - [Fact] - public void LoadUserGroup_ShouldLoadAllGroups() - { - var schemaMock = new Mock(); - var ldapConnectionMock = new Mock(); - var groupLoaderFactoryMock = new Mock(); - var membershipCheckerFactoryMock = new Mock(); - var ldapConnectionFactoryMock = new Mock(); - var loaderMock = new Mock(); - var groupsDns = new DistinguishedName[] { new("cn=group1,dc=example,dc=com"), new("cn=group2,dc=example,dc=com"), new("cn=group3,dc=example,dc=com")}; - loaderMock.Setup(x => x.GetGroups(It.IsAny(), It.IsAny())).Returns(groupsDns); - groupLoaderFactoryMock.Setup(x => x.GetGroupLoader(It.IsAny(), It.IsAny(), It.IsAny())).Returns(loaderMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, membershipCheckerFactoryMock.Object, ldapConnectionFactoryMock.Object); - var actualGroups = service.LoadUserGroups(new LoadUserGroupsRequest(schemaMock.Object, ldapConnectionMock.Object, new DistinguishedName("cn=group1,dc=example,dc=com"), new DistinguishedName("dc=search,dc=base"))); - - var expectedGroups = new[] { "group1", "group2", "group3" }; - Assert.Equal(expectedGroups.Length, actualGroups.Count); - Assert.True(expectedGroups.SequenceEqual(actualGroups)); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void LoadUserGroup_ShouldLoadLimitedNumberOfGroups(int limit) - { - var schemaMock = new Mock(); - var ldapConnectionMock = new Mock(); - var membershipCheckerFactoryMock = new Mock(); - var groupLoaderFactoryMock = new Mock(); - var ldapConnectionFactoryMock = new Mock(); - var loaderMock = new Mock(); - var groupsDns = new DistinguishedName[] { new("cn=group1,dc=example,dc=com"), new("cn=group2,dc=example,dc=com"), new("cn=group3,dc=example,dc=com")}; - loaderMock.Setup(x => x.GetGroups(It.IsAny(), It.IsAny())).Returns(groupsDns); - groupLoaderFactoryMock.Setup(x => x.GetGroupLoader(It.IsAny(), It.IsAny(), It.IsAny())).Returns(loaderMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, membershipCheckerFactoryMock.Object, ldapConnectionFactoryMock.Object); - var actualGroups = service.LoadUserGroups(new LoadUserGroupsRequest(schemaMock.Object, ldapConnectionMock.Object, new DistinguishedName("cn=group1,dc=example,dc=com"), new DistinguishedName("dc=search,dc=base"), limit)); - - var expectedGroups = new string[] { "group1", "group2", "group3" }; - Assert.Equal(limit, actualGroups.Count); - Assert.True(expectedGroups.Take(limit).SequenceEqual(actualGroups)); - } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void LoadUserGroup_InvalidLimit_ShouldThrowException(int limit) - { - var schemaMock = new Mock(); - var ldapConnectionMock = new Mock(); - var membershipCheckerFactoryMock = new Mock(); - var groupLoaderFactory = new Mock(); - var ldapConnectionFactoryMock = new Mock(); - var loaderMock = new Mock(); - var groupsDns = new DistinguishedName[] { new("cn=group1,dc=example,dc=com"), new("cn=group2,dc=example,dc=com"), new("cn=group3,dc=example,dc=com")}; - loaderMock.Setup(x => x.GetGroups(It.IsAny(), It.IsAny())).Returns(groupsDns); - groupLoaderFactory.Setup(x => x.GetGroupLoader(It.IsAny(), It.IsAny(), It.IsAny())).Returns(loaderMock.Object); - - var service = new LdapGroupService(groupLoaderFactory.Object, membershipCheckerFactoryMock.Object, ldapConnectionFactoryMock.Object); - Assert.Throws(() => service.LoadUserGroups(new LoadUserGroupsRequest(schemaMock.Object, ldapConnectionMock.Object, new DistinguishedName("cn=group1,dc=example,dc=com"), new DistinguishedName("dc=search,dc=base") ,limit))); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsFalse_ShouldReturnTrue() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - var memberShipCheckerFactoryMock = new Mock(); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(false); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group1,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.True(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsFalseNoMemberOfValues_ShouldReturnFalse() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - var memberShipCheckerFactoryMock = new Mock(); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(false); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group1,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.False(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsFalse_ShouldReturnFalse() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - var memberShipCheckerFactoryMock = new Mock(); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(false); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group2,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.False(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsTrueNoBaseDns_ShouldReturnTrue() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - connectionFactory.Setup(x=> x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var memberShipCheckerFactoryMock = new Mock(); - var checkerMock = new Mock(); - var namingContext = new DistinguishedName("dc=example,dc=com"); - checkerMock.Setup(x=> x.IsMemberOf(It.IsAny(), It.IsAny())).Returns(true); - memberShipCheckerFactoryMock - .Setup(x => x.GetMembershipChecker(It.IsAny(), It.IsAny(), namingContext)) - .Returns(checkerMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - var schemaMock = new Mock(); - schemaMock.Setup(x => x.NamingContext).Returns(namingContext); - - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(schemaMock.Object); - - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group2,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.True(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsTrueHasBaseDns_ShouldReturnTrue() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - connectionFactory.Setup(x=> x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var memberShipCheckerFactoryMock = new Mock(); - var checkerMock = new Mock(); - var namingContext = new DistinguishedName("dc=example1,dc=com"); - checkerMock.Setup(x=> x.IsMemberOf(It.IsAny(), It.IsAny())).Returns(true); - memberShipCheckerFactoryMock - .Setup(x => x.GetMembershipChecker(It.IsAny(), It.IsAny(), namingContext)) - .Returns(checkerMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([new DistinguishedName("dc=example1,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group2,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.True(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsTrueNoBaseDns_ShouldReturnFalse() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - connectionFactory.Setup(x=> x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var memberShipCheckerFactoryMock = new Mock(); - var checkerMock = new Mock(); - var namingContext = new DistinguishedName("dc=example,dc=com"); - checkerMock.Setup(x=> x.IsMemberOf(It.IsAny(), It.IsAny())).Returns(false); - memberShipCheckerFactoryMock - .Setup(x => x.GetMembershipChecker(It.IsAny(), It.IsAny(), namingContext)) - .Returns(checkerMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - var schemaMock = new Mock(); - schemaMock.Setup(x => x.NamingContext).Returns(namingContext); - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(schemaMock.Object); - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group2,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.False(isMember); - } - - [Fact] - public void IsMemberOf_LoadNestedGroupsIsTrueHasBaseDns_ShouldReturnFalse() - { - var groupLoaderFactoryMock = new Mock(); - var connectionFactory = new Mock(); - connectionFactory.Setup(x=> x.CreateConnection(It.IsAny())).Returns(new Mock().Object); - - var memberShipCheckerFactoryMock = new Mock(); - var checkerMock = new Mock(); - var namingContext = new DistinguishedName("dc=example1,dc=com"); - checkerMock.Setup(x=> x.IsMemberOf(It.IsAny(), It.IsAny())).Returns(false); - memberShipCheckerFactoryMock - .Setup(x => x.GetMembershipChecker(It.IsAny(), It.IsAny(), namingContext)) - .Returns(checkerMock.Object); - - var service = new LdapGroupService(groupLoaderFactoryMock.Object, memberShipCheckerFactoryMock.Object, connectionFactory.Object); - var contextMock = new Mock(); - contextMock.Setup(x => x.UserLdapProfile.Dn).Returns(new DistinguishedName("cn=user,dc=example,dc=com")); - contextMock.Setup(x => x.UserLdapProfile.MemberOf).Returns([new DistinguishedName("cn=group1,dc=example,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.LoadNestedGroups).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.NestedGroupsBaseDns).Returns([new DistinguishedName("dc=example1,dc=com")]); - contextMock.Setup(x => x.LdapServerConfiguration.ConnectionString).Returns("connectionString"); - contextMock.Setup(x => x.LdapServerConfiguration.UserName).Returns("user"); - contextMock.Setup(x => x.LdapServerConfiguration.Password).Returns("password"); - contextMock.Setup(x => x.LdapServerConfiguration.BindTimeoutInSeconds).Returns(10); - contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); - var request = new MembershipRequest(contextMock.Object, [new DistinguishedName("cn=group2,dc=example,dc=com")]); - - var isMember = service.IsMemberOf(request); - Assert.False(isMember); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs deleted file mode 100644 index 467d60d6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs +++ /dev/null @@ -1,1364 +0,0 @@ -# nullable disable -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.MultifactorApi; - -public class MultifactorApiServiceTests -{ - [Fact] - public async Task CreateSecondFactorRequestAsync_EmptyContext_ShouldThrow() - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - await Assert.ThrowsAsync(() => service.CreateSecondFactorRequestAsync(null)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task CreateSecondFactorRequestAsync_NoIdentity_ShouldThrow(string identity) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(identity); - contextMock.Setup(x => x.LdapServerConfiguration.PhoneAttributes).Returns([]); - contextMock.Setup(x => x.RequestPacket.UserName).Returns(identity); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([new LdapAttribute("key", "value")]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task CreateSecondFactorRequestAsync_EmptyIdentityAttributeValue_ShouldThrow(string identity) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns("test"); - contextMock.Setup(x => x.UserLdapProfile.Attributes) - .Returns([new LdapAttribute(new LdapAttributeName("test"), [identity])]); - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_BypassByCache_ShouldReturnBypass() - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - cacheMock.Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())).Returns(true); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Bypass, response.Code); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_NoCallingStationIdAttributeAndBypassByCache_ShouldReturnBypass() - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - cacheMock.Setup(x => x.TryHitCache(It.Is(id => id == "127.0.0.1"), It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Bypass, response.Code); - } - - [Theory] - [InlineData(RequestStatus.AwaitingAuthentication, AuthenticationStatus.Awaiting, false)] - [InlineData(RequestStatus.Granted, AuthenticationStatus.Accept, false)] - [InlineData(RequestStatus.Granted, AuthenticationStatus.Bypass, true)] - [InlineData(RequestStatus.Denied, AuthenticationStatus.Reject, false)] - public async Task CreateSecondFactorRequestAsync_ShouldReturnStatus(RequestStatus status, - AuthenticationStatus expectedStatus, bool bypass) - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = status, Bypassed = bypass }); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(expectedStatus, response.Code); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_ShouldReturnState() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted, Id = "State" }); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal("State", response.State); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_ShouldReturnReplyMessage() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted, ReplyMessage = "Reply Message" }); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal("Reply Message", response.ReplyMessage); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_MultifactorApiUnreachableExceptionNoBypass_ShouldReturnReject() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(false); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_MultifactorApiUnreachableExceptionWithBypass_ShouldReturnBypass() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Bypass, response.Code); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_Exception_ShouldReturnReject() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception()); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - var context = contextMock.Object; - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CreateSecondFactorRequestAsync_MultipleUrlsWithUnreachableUrl_ShouldAccept(bool bypass) - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted }); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(bypass); - - var context = contextMock.Object; - - // Act - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - apiMock.Verify( - x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_MultipleUrlsWithAllUnreachableUrls_ShouldReject() - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(false); - - var context = contextMock.Object; - - // Act - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - apiMock.Verify( - x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Fact] - public async Task CreateSecondFactorRequestAsync_MultipleUrlsWithException_ShouldReject() - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(false); - - var context = contextMock.Object; - - // Act - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - apiMock.Verify( - x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CreateSecondFactorRequestAsync_MultipleUrlsFirstResponse_ShouldAccept(bool bypass) - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.CreateAccessRequest(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted }); - - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .Returns(false); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.ApiUrls).Returns([mfUrl, brokenUrl]); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(bypass); - - var context = contextMock.Object; - - // Act - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(context)); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - apiMock.Verify( - x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(1)); - } - - [Fact] - public async Task SendChallenge_NoContext_ShouldThrowException() - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - await Assert.ThrowsAsync(() => service.SendChallengeAsync(null)); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task SendChallenge_NoAnswer_ShouldThrowException(string answer) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var context = new Mock().Object; - await Assert.ThrowsAnyAsync(() => - service.SendChallengeAsync(new SendChallengeRequest(context, answer, "requestId"))); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task SendChallenge_NoRequestId_ShouldThrowException(string requestId) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var context = new Mock().Object; - await Assert.ThrowsAnyAsync(() => - service.SendChallengeAsync(new SendChallengeRequest(context, "answer", requestId))); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task SendChallenge_NoIdentity_ShouldThrow(string identity) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(false); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(identity); - contextMock.Setup(x => x.RequestPacket.UserName).Returns(identity); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - var context = contextMock.Object; - await Assert.ThrowsAnyAsync(() => service.SendChallengeAsync(new SendChallengeRequest(context, "answer", "requestId"))); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task SendChallenge_EmptyIdentityAttributeValue_ShouldThrow(string identity) - { - var apiMock = new Mock(); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns("test"); - contextMock.Setup(x => x.UserLdapProfile.Attributes) - .Returns([new LdapAttribute(new LdapAttributeName("test"), [identity])]); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var context = contextMock.Object; - await Assert.ThrowsAnyAsync(() => - service.SendChallengeAsync(new SendChallengeRequest(context, "answer", "requestId"))); - } - - [Theory] - [InlineData(RequestStatus.Denied, AuthenticationStatus.Reject)] - [InlineData(RequestStatus.Granted, AuthenticationStatus.Accept)] - [InlineData(RequestStatus.Granted, AuthenticationStatus.Bypass, true)] - [InlineData(RequestStatus.AwaitingAuthentication, AuthenticationStatus.Awaiting)] - public async Task SendChallenge_ShouldReturnResponseCode(RequestStatus requestStatus, AuthenticationStatus expectedStatus, bool bypassed = false) - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = requestStatus, Bypassed = bypassed }); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal(expectedStatus, response.Code); - } - - [Fact] - public async Task SendChallenge_ShouldReturnState() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = RequestStatus.Granted, Id = "State" }); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal("State", response.State); - } - - [Fact] - public async Task SendChallenge_ShouldReturnReplyMessage() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = RequestStatus.Granted, ReplyMessage = "Reply Message"}); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal("Reply Message", response.ReplyMessage); - } - - [Fact] - public async Task SendChallenge_MultifactorApiUnreachableExceptionNoBypass_ShouldReturnReject() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(false); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Fact] - public async Task SendChallenge_MultifactorApiUnreachableExceptionBypass_ShouldReturnBypass() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ThrowsAsync(new MultifactorApiUnreachableException()); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Bypass, response.Code); - } - - [Fact] - public async Task SendChallenge_Exception_ShouldReturnReject() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), - It.IsAny())) - .ThrowsAsync(new Exception()); - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.UserLdapProfile).Returns(new Mock().Object); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(true); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SendChallenge_MultipleUrlsWithUnreachableUrl_ShouldAccept(bool bypass) - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = RequestStatus.Granted }); - - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(bypass); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - apiMock.Verify( - x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Fact] - public async Task SendChallenge_MultipleUrlsWithAllUnreachableUrls_ShouldReject() - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - - // Act - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - apiMock.Verify( - x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Fact] - public async Task SendChallenge_MultipleUrlsWithException_ShouldReject() - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - var cacheMock = new Mock(); - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns([brokenUrl, mfUrl]); - - // Act - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Reject, response.Code); - apiMock.Verify( - x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SendChallenge_MultipleUrlsFirstResponse_ShouldAccept(bool bypass) - { - // Arrange - var brokenUrl = "broken-url.com"; - var mfUrl = "mf-url.dev"; - var apiMock = new Mock(); - - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == brokenUrl), It.IsAny(), - It.IsAny())) - .Throws(); - - apiMock - .Setup(x => x.SendChallengeAsync(It.Is(u => u == mfUrl), It.IsAny(), - It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() - { - Status = RequestStatus.Granted - }); - - var cacheMock = new Mock(); - - var service = - new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime) - .Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.BypassSecondFactorWhenApiUnreachable).Returns(bypass); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns([mfUrl, brokenUrl]); - - // Act - var response = - await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - - // Assert - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - apiMock.Verify( - x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Exactly(1)); - } - - [Fact] - public async Task CreateSecondFactorRequest_AuthenticationCacheEnabled_ShouldSetAuthenticationCache() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted, Bypassed = false }); - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(false); - - var service = new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.AuthenticationCacheGroups).Returns([]); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(contextMock.Object, cacheEnabled: true)); - - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - cacheMock.Verify(x => x.SetCache(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateSecondFactorRequest_AuthenticationCacheDisabled_ShouldNotSetAuthenticationCache() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.CreateAccessRequest(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AccessRequestResponse() { Status = RequestStatus.Granted, Bypassed = false }); - var cacheMock = new Mock(); - cacheMock - .Setup(x => x.TryHitCache(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(false); - - var service = new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.TryGetChallenge()).Returns(() => null); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.UserLdapProfile.Attributes).Returns([]); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.ClientConfigurationName).Returns("configName"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("123", "123")); - contextMock.Setup(x => x.LdapServerConfiguration.AuthenticationCacheGroups).Returns([]); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = await service.CreateSecondFactorRequestAsync(new CreateSecondFactorRequest(contextMock.Object, cacheEnabled: false)); - - Assert.NotNull(response); - Assert.Equal(AuthenticationStatus.Accept, response.Code); - cacheMock.Verify(x => x.SetCache(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Never); - } - - [Fact] - public async Task SendChallenge_AuthenticationCacheEnabled_ShouldSetAuthenticationCache() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = RequestStatus.Granted, Bypassed = false }); - var cacheMock = new Mock(); - var service = new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId")); - Assert.NotNull(response); - cacheMock.Verify(x => x.SetCache(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendChallenge_AuthenticationCacheDisabled_ShouldSetAuthenticationCache() - { - var apiMock = new Mock(); - apiMock - .Setup(x => x.SendChallengeAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(() => new AccessRequestResponse() { Status = RequestStatus.Granted, Bypassed = false }); - var cacheMock = new Mock(); - var service = new MultifactorApiService(apiMock.Object, cacheMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.RequestPacket.CalledStationIdAttribute).Returns("CalledStationIdAttribute"); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("CallingStationIdAttribute"); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.UserLdapProfile.DisplayName).Returns("123"); - contextMock.Setup(x => x.UserLdapProfile.Email).Returns("email"); - contextMock.Setup(x => x.UserLdapProfile.Phone).Returns("phone"); - contextMock.Setup(x => x.ClientConfigurationName).Returns("config"); - contextMock.Setup(x => x.AuthenticationCacheLifetime).Returns(AuthenticatedClientCacheConfig.Create("08:08:08")); - contextMock.Setup(x => x.PrivacyMode).Returns(PrivacyModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.UserNameTransformRules).Returns(new UserNameTransformRules()); - contextMock.Setup(x => x.LdapServerConfiguration.IdentityAttribute).Returns(string.Empty); - contextMock.Setup(x => x.RequestPacket.UserName).Returns("username"); - contextMock.Setup(x => x.ApiCredential).Returns(new ApiCredential("key", "secret")); - contextMock.Setup(x => x.ApiUrls).Returns(["url"]); - - var response = await service.SendChallengeAsync(new SendChallengeRequest(contextMock.Object, "answer", "requestId", false)); - Assert.NotNull(response); - cacheMock.Verify(x => x.SetCache(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny()), Times.Never); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs deleted file mode 100644 index 0bc2c5f6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Http; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.MultifactorApi; - -public class MultifactorApiTests -{ - [Fact] - public async Task SendRequest_EmptyPayload_ShouldThrowException() - { - var clientMock = new Mock(); - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => api.CreateAccessRequest("url", null, new ApiCredential("key", "secret"))); - } - - [Fact] - public async Task SendRequest_EmptyApiCredential_ShouldThrowException() - { - var clientMock = new Mock(); - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => api.CreateAccessRequest("url", new AccessRequest(), null)); - } - - [Fact] - public async Task SendRequest_EmptyHttpResponse_ShouldDenied() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .ReturnsAsync(() => null); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - var response = await api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret")); - Assert.NotNull(response); - Assert.Equal(RequestStatus.Denied, response.Status); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task SendRequest_ShouldReturnResponse(bool success) - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .ReturnsAsync(() => new MultiFactorApiResponse() { Success = success, Model = new AccessRequestResponse() {Status = RequestStatus.Granted} } ); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - var response = await api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret")); - Assert.NotNull(response); - Assert.Equal(RequestStatus.Granted, response.Status); - } - - - [Fact] - public async Task SendRequest_HttpRequestException_ShouldMultifactorApiUnreachableException() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .Throws(new HttpRequestException()); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret"))); - } - - [Fact] - public async Task SendRequest_TooManyRequests_ShouldReturnDenied() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .Throws(new HttpRequestException(string.Empty, null, HttpStatusCode.TooManyRequests)); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - var response = await api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret")); - Assert.NotNull(response); - Assert.Equal(RequestStatus.Denied, response.Status); - } - - [Fact] - public async Task SendRequest_TaskCanceledException_ShouldReturnDenied() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .Throws(new TaskCanceledException()); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - var response = await api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret")); - Assert.NotNull(response); - Assert.Equal(RequestStatus.Denied, response.Status); - } - - [Fact] - public async Task SendChallenge_TaskCanceledException_ShouldReturnDenied() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .Throws(new TaskCanceledException()); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - var response = await api.SendChallengeAsync("url", new ChallengeRequest(), new ApiCredential("key", "secret")); - Assert.NotNull(response); - Assert.Equal(RequestStatus.Denied, response.Status); - } - - [Fact] - public async Task SendRequest_Exception_ShouldMultifactorApiUnreachableException() - { - var clientMock = new Mock(); - - clientMock - .Setup(x => x.PostAsync>(It.IsAny(), It.IsAny(), It.IsAny>())) - .Throws(new Exception()); - - var api = new Services.MultifactorApi.MultifactorApi(clientMock.Object, NullLogger.Instance); - await Assert.ThrowsAsync(() => api.CreateAccessRequest("url", new AccessRequest(), new ApiCredential("key", "secret"))); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs deleted file mode 100644 index 64537f8d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.PipelineSteps; - -public class IpWhiteListStepTests -{ - [Fact] - public async Task EmptyWhiteList_ShouldNotTerminatePipeline() - { - var step = new IpWhiteListStep(NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - var executionState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(executionState); - contextMock.Setup(x => x.IpWhiteList).Returns([]); - var context = contextMock.Object; - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - Assert.False(executionState.IsTerminated); - } - - [Fact] - public async Task ClientIpInRange_ShouldNotTerminatePipeline() - { - var step = new IpWhiteListStep(NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - var executionState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(executionState); - contextMock.Setup(x => x.IpWhiteList).Returns([IPAddressRange.Parse("127.0.0.1")]); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns(string.Empty); - var context = contextMock.Object; - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - Assert.False(executionState.IsTerminated); - } - - [Fact] - public async Task ClientIpNotInRange_ShouldTerminate() - { - var step = new IpWhiteListStep(NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - var executionState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(executionState); - contextMock.Setup(x => x.IpWhiteList).Returns([IPAddressRange.Parse("127.0.0.1")]); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.2")); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns(string.Empty); - var context = contextMock.Object; - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Reject, authState.SecondFactorStatus); - Assert.True(executionState.IsTerminated); - } - - [Fact] - public async Task CallingStationIdInRange_ShouldNotTerminatePipeline() - { - var step = new IpWhiteListStep(NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - var executionState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(executionState); - contextMock.Setup(x => x.IpWhiteList).Returns([IPAddressRange.Parse("127.0.0.1")]); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.2")); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("127.0.0.1"); - var context = contextMock.Object; - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - Assert.False(executionState.IsTerminated); - } - - [Fact] - public async Task CallingStationIdNotInRange_ShouldTerminate() - { - var step = new IpWhiteListStep(NullLogger.Instance); - - var contextMock = new Mock(); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - var executionState = new ExecutionState(); - contextMock.Setup(x => x.ExecutionState).Returns(executionState); - contextMock.Setup(x => x.IpWhiteList).Returns([IPAddressRange.Parse("127.0.0.1")]); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1")); - contextMock.Setup(x => x.RequestPacket.CallingStationIdAttribute).Returns("127.0.0.2"); - var context = contextMock.Object; - await step.ExecuteAsync(context); - - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Reject, authState.SecondFactorStatus); - Assert.True(executionState.IsTerminated); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs deleted file mode 100644 index d071bb81..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit.PipelineTests.StepsTests; - -public class UserNameValidationStepTests -{ - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task ExecuteAsync_EmptyUserName_ShouldCompleteStep(string userName) - { - //Arrange - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var step = new UserNameValidationStep(NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns(userName); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.False(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } - - [Fact] - public async Task ExecuteAsync_NoServerConfiguration_ShouldCompleteStep() - { - //Arrange - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var step = new UserNameValidationStep(NullLogger.Instance); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns("userName"); - contextMock.Setup(x=> x.LdapServerConfiguration).Returns(() => null); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.False(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } - - [Theory] - [InlineData("name")] - [InlineData("domain/name")] - [InlineData("cn=user,dc=domain,dc=com")] - public async Task ExecuteAsync_UpnRequiredAndUserNameNotUpn_ShouldTerminatePipeline(string userName) - { - //Arrange - var step = new UserNameValidationStep(NullLogger.Instance); - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.UpnRequired).Returns(true); - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns(userName); - contextMock.Setup(x=> x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.True(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } - - [Theory] - [InlineData("name")] - [InlineData("domain/name")] - [InlineData("cn=user,dc=domain,dc=com")] - public async Task ExecuteAsync_UpnNotRequiredAndUserNameNotUpn_ShouldCompleteStep(string userName) - { - //Arrange - var step = new UserNameValidationStep(NullLogger.Instance); - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.UpnRequired).Returns(false); - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns(userName); - contextMock.Setup(x=> x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.False(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task ExecuteAsync_UserNameSuffixPermitted_ShouldCompleteStep(bool upnRequired) - { - //Arrange - var step = new UserNameValidationStep(NullLogger.Instance); - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.UpnRequired).Returns(upnRequired); - serverConfigMock.Setup(x => x.SuffixesPermissions).Returns(new PermissionRules()); - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns("user@domain.com"); - contextMock.Setup(x=> x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.False(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Awaiting, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task ExecuteAsync_UserNameSuffixNotPermitted_ShouldTerminatePipeline(bool upnRequired) - { - //Arrange - var step = new UserNameValidationStep(NullLogger.Instance); - var serverConfigMock = new Mock(); - serverConfigMock.Setup(x => x.UpnRequired).Returns(upnRequired); - serverConfigMock.Setup(x => x.SuffixesPermissions).Returns(new PermissionRules(new List(){"domain2"}, new List())); - var execState = new ExecutionState(); - var authState = new AuthenticationState(); - var responseInfo = new ResponseInformation(); - var contextMock = new Mock(); - contextMock.Setup(x=> x.RequestPacket.UserName).Returns("user@domain.com"); - contextMock.Setup(x=> x.LdapServerConfiguration).Returns(serverConfigMock.Object); - contextMock.Setup(x => x.ResponseInformation).Returns(responseInfo); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.ExecutionState).Returns(execState); - var context = contextMock.Object; - - //Act - await step.ExecuteAsync(context); - - //Assert - Assert.True(execState.IsTerminated); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal(AuthenticationStatus.Awaiting, authState.SecondFactorStatus); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs deleted file mode 100644 index 6ead5c83..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Globalization; -using System.Net; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit; - -public class RadiusAttributeTypeConverterTests -{ - [Fact] - public void ConvertString() - { - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converter = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var radiusAttribute = converter.ConvertType("key", "string"); - Assert.NotNull(radiusAttribute); - Assert.Equal("string", radiusAttribute as string); - } - - [Fact] - public void ConvertIpaddr() - { - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "ipaddr")); - var converter = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var radiusAttribute = converter.ConvertType("key", "127.0.0.1"); - Assert.NotNull(radiusAttribute); - Assert.Equal(IPAddress.Parse("127.0.0.1"), radiusAttribute as IPAddress); - } - - [Fact] - public void ConvertDate() - { - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "date")); - var converter = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - DateTime.TryParse("12.10.2025", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateValue); - var radiusAttribute = converter.ConvertType("key", dateValue.Date.ToString(CultureInfo.InvariantCulture)); - - Assert.NotNull(radiusAttribute); - Assert.Equal(dateValue, radiusAttribute); - } - - [Fact] - public void ConvertInteger() - { - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "integer")); - var converter = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var radiusAttribute = converter.ConvertType("key", 123); - - Assert.NotNull(radiusAttribute); - Assert.Equal(123, radiusAttribute); - } - - [Fact] - public void ConvertMsRadiusFramedIpAddress() - { - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "ipaddr")); - var converter = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var radiusAttribute = converter.ConvertType("key", "-1235"); - - Assert.NotNull(radiusAttribute); - Assert.Equal(IPAddress.Parse("255.255.251.45"), radiusAttribute as IPAddress); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs deleted file mode 100644 index 4c6255cd..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs +++ /dev/null @@ -1,261 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.Unit; - -public class RadiusReplyAttributeServiceTests -{ - [Fact] - public void NullRequest_ShouldThrowArgumentNullException() - { - var radiusDictionaryMock = new Mock(); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - Assert.Throws(() => service.GetReplyAttributes(null)); - } - - [Fact] - public void NoReplyAttributes_ShouldReturnEmptyCollection() - { - var radiusDictionaryMock = new Mock(); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - new Dictionary(), - new List()); - - var result = service.GetReplyAttributes(request); - Assert.Empty(result); - } - - [Fact] - public void GetReplyAttributes_ConstantAttribute_ShouldReturnReplyAttributes() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "")]); - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - new List()); - - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - var result = service.GetReplyAttributes(request); - Assert.Single(result); - var replyAttrVal = result.First().Value.FirstOrDefault(); - Assert.NotNull(replyAttrVal); - Assert.Equal("const", replyAttrVal as string); - } - - [Fact] - public void GetReplyAttributes_FromLdapAttribute_ShouldReturnReplyAttributes() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const")]); - - var ldapAttributes = new List() { new("const", "fromLdap") }; - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - ldapAttributes); - - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - var result = service.GetReplyAttributes(request); - Assert.Single(result); - var replyAttrVal = result.First().Value.FirstOrDefault(); - Assert.NotNull(replyAttrVal); - Assert.Equal("fromLdap", replyAttrVal as string); - } - - [Fact] - public void GetReplyAttributes_NoLdapAttribute_ShouldReturnEmptyValues() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const")]); - - var ldapAttributes = new List() { new("const2", "fromLdap") }; - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - ldapAttributes); - - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - var result = service.GetReplyAttributes(request); - var attr = result.First().Value; - Assert.Empty(attr); - } - - [Fact] - public void GetReplyAttributes_MemberOfAttribute_ShouldReturnReplyAttributes() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("memberof")]); - var userGroups = new HashSet(); - userGroups.Add("group1"); - var request = new GetReplyAttributesRequest( - "userName", - userGroups, - replyAttributes, - new List()); - - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - var result = service.GetReplyAttributes(request); - var attr = result.First().Value.FirstOrDefault(); - Assert.Equal("group1", attr as string); - } - - [Fact] - public void GetReplyAttributes_NoMemberOfAttribute_ShouldReturnEmptyValues() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("memberof")]); - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - new List()); - - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - var result = service.GetReplyAttributes(request); - var attr = result.First().Value; - Assert.Empty(attr); - } - - [Fact] - public void GetReplyAttributes_UserNameCondition_ShouldReturnReplyAttributes() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "UserName=userName")]); - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - new List()); - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - var result = service.GetReplyAttributes(request); - var attr = result.First().Value.FirstOrDefault(); - Assert.Equal("const", attr as string); - } - - [Fact] - public void GetReplyAttributes_InappropriateUserNameCondition_ShouldReturnEmptyValues() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "UserName=userName1")]); - var request = new GetReplyAttributesRequest( - "userName", - new HashSet(), - replyAttributes, - new List()); - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - var result = service.GetReplyAttributes(request); - var attr = result.First().Value; - - Assert.Empty(attr); - } - - [Fact] - public void GetReplyAttributes_UserGroupCondition_ShouldReturnReplyAttributes() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "UserGroup=group1")]); - var userGroups = new HashSet(); - userGroups.Add("group1"); - var request = new GetReplyAttributesRequest( - "userName", - userGroups, - replyAttributes, - new List()); - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - var result = service.GetReplyAttributes(request); - var attr = result.First().Value.FirstOrDefault(); - Assert.Equal("const", attr as string); - } - - [Fact] - public void GetReplyAttributes_InappropriateUserGroupCondition_ShouldReturnEmptyValues() - { - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "UserGroup=group2")]); - var userGroups = new HashSet(); - userGroups.Add("group1"); - var request = new GetReplyAttributesRequest( - "userName", - userGroups, - replyAttributes, - new List()); - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - var result = service.GetReplyAttributes(request); - var attr = result.First().Value; - - Assert.Empty(attr); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void GetReplyAttributes_UserNameConditionAndEmptyUserName_ShouldReturnEmptyAttributes(string emptyString) - { - //Arrange - var replyAttributes = new Dictionary(); - replyAttributes.Add("key", [new RadiusReplyAttributeValue("const", "UserName=userName")]); - var request = new GetReplyAttributesRequest( - emptyString, - new HashSet(), - replyAttributes, - new List()); - var radiusDictionaryMock = new Mock(); - radiusDictionaryMock.Setup(x => x.GetAttribute("key")).Returns(new DictionaryAttribute("key", 1, "string")); - var converterMock = new RadiusAttributeTypeConverter(radiusDictionaryMock.Object); - var service = new RadiusReplyAttributeService(converterMock, NullLogger.Instance); - - // Act - var result = service.GetReplyAttributes(request); - - //Assert - Assert.Single(result); - var attr = result.First().Value; - Assert.Empty(attr); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs deleted file mode 100644 index 2000a6c0..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; - -namespace Multifactor.Radius.Adapter.v2.Tests.UserIdentityTests; - -public class UserIdentityTests -{ - [Theory] - [InlineData("user@domain", UserIdentityFormat.UserPrincipalName)] - [InlineData("cn=user,dc=domain", UserIdentityFormat.DistinguishedName)] - [InlineData("domain\\user", UserIdentityFormat.NetBiosName)] - [InlineData("user", UserIdentityFormat.SamAccountName)] - public void UserIdentity_ShouldCreateIdentity(string name, UserIdentityFormat userIdentityFormat) - { - var identity = new UserIdentity(name, userIdentityFormat); - Assert.Equal(name, identity.Identity); - Assert.Equal(userIdentityFormat, identity.Format); - } - - [Theory] - [InlineData("user@domain", UserIdentityFormat.UserPrincipalName)] - [InlineData("cn=user,dc=domain", UserIdentityFormat.DistinguishedName)] - [InlineData("domain\\user", UserIdentityFormat.NetBiosName)] - [InlineData("user", UserIdentityFormat.SamAccountName)] - public void UserIdentity_ShouldParseIdentity(string name, UserIdentityFormat userIdentityFormat) - { - var identity = new UserIdentity(name); - Assert.Equal(name, identity.Identity); - Assert.Equal(userIdentityFormat, identity.Format); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public void UserIdentity_EmptyIdentity_ShouldThrowArgumentException(string name) - { - Assert.ThrowsAny(() => new UserIdentity(name)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeStatus.cs deleted file mode 100644 index 7a48fddf..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeStatus.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public enum ChallengeStatus -{ - Reject = 0, - InProcess, - Accept -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeType.cs deleted file mode 100644 index ac66a8b2..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public enum ChallengeType -{ - None = 0, - SecondFactor, - PasswordChange -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs deleted file mode 100644 index 85662e99..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.DataProtection; -using Multifactor.Radius.Adapter.v2.Services.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public class ChangePasswordChallengeProcessor : IChallengeProcessor -{ - private readonly ICacheService _cache; - private readonly ILdapProfileService _ldapService; - private readonly IDataProtectionService _dataProtectionService; - private readonly ILogger _logger; - - public ChangePasswordChallengeProcessor( - ICacheService cache, - ILdapProfileService ldapService, - IDataProtectionService dataProtectionService, - ILogger logger) - { - _cache = cache; - _ldapService = ldapService; - _dataProtectionService = dataProtectionService; - _logger = logger; - } - - public ChallengeType ChallengeType => ChallengeType.PasswordChange; - - public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext context) - { - ArgumentNullException.ThrowIfNull(context); - if (string.IsNullOrWhiteSpace(context.Passphrase.Password)) - throw new InvalidOperationException("User password is required."); - - if (string.IsNullOrWhiteSpace(context.MustChangePasswordDomain)) - throw new InvalidOperationException("Domain is required."); - - var encryptedPassword = _dataProtectionService.Protect(context.ApiCredential.Pwd, context.Passphrase.Password); - - var passwordRequest = new PasswordChangeRequest() - { - Domain = context.MustChangePasswordDomain, - CurrentPasswordEncryptedData = encryptedPassword - }; - - _cache.Set(passwordRequest.Id, passwordRequest, DateTimeOffset.UtcNow.AddMinutes(5)); - _logger.LogInformation($"Password change state: \"{passwordRequest.Id}\""); - context.ResponseInformation.State = passwordRequest.Id; - context.ResponseInformation.ReplyMessage = "Please change password to continue. Enter new password: "; - return new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); - } - - public bool HasChallengeContext(ChallengeIdentifier identifier) => _cache.TryGetValue(identifier.RequestId, out _); - - public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, IRadiusPipelineExecutionContext context) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.UserLdapProfile); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration); - ArgumentNullException.ThrowIfNull(context.LdapSchema); - - var passwordChangeRequest = GetPasswordChangeRequest(identifier.RequestId); - if (passwordChangeRequest == null) - return ChallengeStatus.Accept; - - if (string.IsNullOrWhiteSpace(context.Passphrase.Raw)) - { - context.ResponseInformation.ReplyMessage = "Password is empty"; - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - return ChallengeStatus.Reject; - } - - if (string.IsNullOrWhiteSpace(passwordChangeRequest.NewPasswordEncryptedData)) - return RepeatPasswordChallenge(context, passwordChangeRequest); - - var decryptedNewPassword = _dataProtectionService.Unprotect(context.ApiCredential.Pwd, passwordChangeRequest.NewPasswordEncryptedData); - if (decryptedNewPassword != context.Passphrase.Raw) - return PasswordsNotMatchChallenge(context, passwordChangeRequest); - - var request = new ChangeUserPasswordRequest( - decryptedNewPassword, - context.UserLdapProfile, - context.LdapServerConfiguration, - context.LdapSchema); - - var result = await _ldapService.ChangeUserPasswordAsync(request); - - _cache.Remove(passwordChangeRequest.Id); - context.ResponseInformation.State = null; - - if (result.Success) - return ChallengeStatus.Accept; - - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - - return ChallengeStatus.Reject; - } - - private PasswordChangeRequest? GetPasswordChangeRequest(string id) - { - _cache.TryGetValue(id, out PasswordChangeRequest? passwordChangeRequest); - return passwordChangeRequest; - } - - private ChallengeStatus PasswordsNotMatchChallenge(IRadiusPipelineExecutionContext request, PasswordChangeRequest passwordChangeRequest) - { - passwordChangeRequest.NewPasswordEncryptedData = null; - - _cache.Set(passwordChangeRequest.Id, passwordChangeRequest, DateTimeOffset.UtcNow.AddMinutes(5)); - - request.ResponseInformation.State = passwordChangeRequest.Id; - request.ResponseInformation.ReplyMessage = "Passwords not match. Please enter new password: "; - - return ChallengeStatus.InProcess; - } - - private ChallengeStatus RepeatPasswordChallenge(IRadiusPipelineExecutionContext context, PasswordChangeRequest passwordChangeRequest) - { - passwordChangeRequest.NewPasswordEncryptedData = _dataProtectionService.Protect(context.ApiCredential.Pwd, context.Passphrase.Raw!); - - _cache.Set(passwordChangeRequest.Id, passwordChangeRequest, DateTimeOffset.UtcNow.AddMinutes(5)); - - context.ResponseInformation.State = passwordChangeRequest.Id; - context.ResponseInformation.ReplyMessage = "Please repeat new password: "; - - return ChallengeStatus.InProcess; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessor.cs deleted file mode 100644 index 4128efe3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public interface IChallengeProcessor -{ - //TODO DO NOT change context. Must return some response with required data - ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext context); - bool HasChallengeContext(ChallengeIdentifier identifier); - Task ProcessChallengeAsync(ChallengeIdentifier identifier, IRadiusPipelineExecutionContext context); - public ChallengeType ChallengeType { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessorProvider.cs deleted file mode 100644 index bfb8f97f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessorProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public interface IChallengeProcessorProvider -{ - IChallengeProcessor? GetChallengeProcessorByIdentifier(ChallengeIdentifier identifier); - IChallengeProcessor? GetChallengeProcessorByType(ChallengeType type); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs deleted file mode 100644 index 246a87b3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System.Collections.Concurrent; -using System.Text; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; - -public class SecondFactorChallengeProcessor : IChallengeProcessor -{ - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary _challengeContexts = new(); - private readonly IMultifactorApiService _apiService; - private readonly ILdapGroupService _ldapGroupService; - private readonly ILogger _logger; - - public ChallengeType ChallengeType => ChallengeType.SecondFactor; - - public SecondFactorChallengeProcessor(IMultifactorApiService apiAdapter, ILdapGroupService groupService, ILogger logger) - { - _apiService = apiAdapter; - _ldapGroupService = groupService; - _logger = logger; - } - - public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext context) - { - ArgumentNullException.ThrowIfNull(context, nameof(context)); - ArgumentException.ThrowIfNullOrWhiteSpace(context.ResponseInformation.State); - - var id = new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); - if (_challengeContexts.TryAdd(id, context)) - { - _logger.LogInformation("Challenge {State:l} was added for message id={id}", id.RequestId, context.RequestPacket.Identifier); - return id; - } - - _logger.LogError("Unable to cache request id={id} for the '{cfg:l}' configuration", context.RequestPacket.Identifier, context.ClientConfigurationName); - return ChallengeIdentifier.Empty; - } - - public bool HasChallengeContext(ChallengeIdentifier identifier) => _challengeContexts.ContainsKey(identifier); - - public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, IRadiusPipelineExecutionContext context) - { - _logger.LogInformation("Processing challenge {State:l} for message id={id} from {host:l}:{port}", - identifier.RequestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - - var userName = context.RequestPacket.UserName; - if (string.IsNullOrWhiteSpace(userName)) - return ProcessEmptyName(context, identifier.RequestId); - - var challengeStatus = ProcessAuthenticationType(context, context.Passphrase, identifier.RequestId, out var userAnswer); - if (challengeStatus == ChallengeStatus.Reject) - return challengeStatus; - - var challengeContext = GetChallengeContext(identifier) ?? throw new InvalidOperationException($"Challenge context with identifier '{identifier}' was not found"); - var shouldCacheResponse = ShouldCacheResponse(context); - var response = await _apiService.SendChallengeAsync(new SendChallengeRequest(challengeContext, userAnswer!, identifier.RequestId, shouldCacheResponse)); - - return ProcessResponse(context, challengeContext, response, identifier); - } - - - private IRadiusPipelineExecutionContext? GetChallengeContext(ChallengeIdentifier identifier) - { - if (_challengeContexts.TryGetValue(identifier, out IRadiusPipelineExecutionContext? request)) - return request; - - _logger.LogError("Unable to get cached request with state={identifier:l}", identifier); - return null; - } - - private void RemoveChallengeContext(ChallengeIdentifier identifier) - { - _challengeContexts.TryRemove(identifier, out _); - } - - private ChallengeStatus ProcessAuthenticationType(IRadiusPipelineExecutionContext context, UserPassphrase passphrase, string requestId, out string? userAnswer) - { - userAnswer = string.Empty; - switch (context.RequestPacket.AuthenticationType) - { - case AuthenticationType.PAP: - //user-password attribute holds second request challenge from user - userAnswer = passphrase.Raw; - - if (string.IsNullOrWhiteSpace(userAnswer)) - { - _logger.LogWarning( - "Can't find User-Password with user response in message id={id} from {host:l}:{port}", - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ResponseInformation.State = requestId; - - return ChallengeStatus.Reject; - } - - return ChallengeStatus.InProcess; - case AuthenticationType.MSCHAP2: - var msChapResponse = context.RequestPacket.GetAttribute("MS-CHAP2-Response"); - - if (msChapResponse == null) - { - _logger.LogWarning( - "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Can't find MS-CHAP2-Response", - requestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ResponseInformation.State = requestId; - - return ChallengeStatus.Reject; - } - - //forti behaviour - var otpData = msChapResponse.Skip(2).Take(6).ToArray(); - userAnswer = Encoding.ASCII.GetString(otpData); - return ChallengeStatus.InProcess; - default: - _logger.LogWarning( - "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Unsupported authentication type '{Auth}'", - requestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port, - context.RequestPacket.AuthenticationType); - - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ResponseInformation.State = requestId; - - return ChallengeStatus.Reject; - } - } - - private ChallengeStatus ProcessResponse(IRadiusPipelineExecutionContext context, IRadiusPipelineExecutionContext challengeContext, MultifactorResponse response, ChallengeIdentifier identifier) - { - context.ResponseInformation.ReplyMessage = response.ReplyMessage; - switch (response.Code) - { - case AuthenticationStatus.Accept: - context.ResponsePacket = challengeContext.ResponsePacket; - context.UserLdapProfile = challengeContext.UserLdapProfile; - context.AuthenticationState.FirstFactorStatus = challengeContext.AuthenticationState.FirstFactorStatus; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; - context.Passphrase = challengeContext.Passphrase; - - RemoveChallengeContext(identifier); - - _logger.LogDebug( - "Challenge {State:l} was processed for message id={id} from {host:l}:{port} with result '{Result}'", - identifier.RequestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port, - response.Code); - - return ChallengeStatus.Accept; - - case AuthenticationStatus.Reject: - RemoveChallengeContext(identifier); - _logger.LogDebug( - "Challenge {State:l} was processed for message id={id} from {host:l}:{port} with result '{Result}'", - identifier.RequestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port, - response.Code); - - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ResponseInformation.State = identifier.RequestId; - - return ChallengeStatus.Reject; - - default: - context.ResponseInformation.State = identifier.RequestId; - - return ChallengeStatus.InProcess; - } - } - - private ChallengeStatus ProcessEmptyName(IRadiusPipelineExecutionContext context, string requestId) - { - _logger.LogWarning( - "Unable to process challenge {State:l} for message id={id} from {host:l}:{port}: Can't find User-Name", - requestId, - context.RequestPacket.Identifier, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ResponseInformation.State = requestId; - - return ChallengeStatus.Reject; - } - - private bool ShouldCacheResponse(IRadiusPipelineExecutionContext context) - { - if (context.LdapServerConfiguration is null || context.LdapServerConfiguration.AuthenticationCacheGroups.Count == 0) - return true; - - var cacheGroups = context.LdapServerConfiguration.AuthenticationCacheGroups; - var isMember = _ldapGroupService.IsMemberOf(new MembershipRequest(context, cacheGroups)); - var groupsStr = string.Join(',', cacheGroups); - var username = context.RequestPacket.UserName; - if (!isMember) - _logger.LogDebug("User '{userName}' is not a member of any authentication cache groups: ({groups})", username, groupsStr); - else - _logger.LogDebug("User '{userName}' is a member of authentication cache groups: ({groups})", username, groupsStr); - - return isMember; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticatedClientCacheConfig.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticatedClientCacheConfig.cs deleted file mode 100644 index 78f77485..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticatedClientCacheConfig.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth; - -public record AuthenticatedClientCacheConfig -{ - public TimeSpan Lifetime { get; } - public bool Enabled => Lifetime != TimeSpan.Zero; - - public static AuthenticatedClientCacheConfig Default => new(TimeSpan.Zero); - - public AuthenticatedClientCacheConfig(TimeSpan lifetime) - { - Lifetime = lifetime; - } - - public static AuthenticatedClientCacheConfig Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Default; - } - - return new AuthenticatedClientCacheConfig(TimeSpan.ParseExact(value, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None)); - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationState.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationState.cs deleted file mode 100644 index 14c14d4f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationState.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth; - -public class AuthenticationState : IAuthenticationState -{ - public AuthenticationStatus FirstFactorStatus { get; set; } - public AuthenticationStatus SecondFactorStatus { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/IAuthenticationState.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/IAuthenticationState.cs deleted file mode 100644 index f28281d1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/IAuthenticationState.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth; - -public interface IAuthenticationState -{ - public AuthenticationStatus FirstFactorStatus { get; set; } - - public AuthenticationStatus SecondFactorStatus { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeDescriptor.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeDescriptor.cs deleted file mode 100644 index 6e3e92f3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeDescriptor.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -public class PreAuthModeDescriptor -{ - public PreAuthMode Mode { get; } - public PreAuthModeSettings Settings { get; } - - public static PreAuthModeDescriptor Default => new(PreAuthMode.None, PreAuthModeSettings.Default); - - private PreAuthModeDescriptor(PreAuthMode mode, PreAuthModeSettings settings) - { - Mode = mode; - Settings = settings; - } - - public static PreAuthModeDescriptor Create(string value, PreAuthModeSettings settings) - { - if (settings is null) - { - throw new ArgumentNullException(nameof(settings)); - } - - if (string.IsNullOrWhiteSpace(value)) - { - return new PreAuthModeDescriptor(PreAuthMode.None, settings); - } - - var mode = GetMode(value); - return new PreAuthModeDescriptor(mode, settings); - } - - private static PreAuthMode GetMode(string value) - { - var parse = Enum.Parse(value, true); - return parse; - } - - public override string ToString() => Mode.ToString(); - - public static string DisplayAvailableModes() => string.Join(", ", Enum.GetNames(typeof(PreAuthMode))); -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeSettings.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeSettings.cs deleted file mode 100644 index a8a0e25f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeSettings.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -public class PreAuthModeSettings -{ - public int OtpCodeLength { get; } - public string OtpCodeRegex { get; } - - public PreAuthModeSettings(int otpCodeLength) - { - if (otpCodeLength < 1 || otpCodeLength > 20) - { - throw new ArgumentOutOfRangeException(nameof(otpCodeLength), "Value should not be less than 1 and should not be more than 20"); - } - OtpCodeLength = otpCodeLength; - OtpCodeRegex = $"^[0-9]{{{otpCodeLength}}}$"; - } - - public static PreAuthModeSettings Default => new(6); -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/UserNameTransformRules.cs b/src/Multifactor.Radius.Adapter.v2/Core/Auth/UserNameTransformRules.cs deleted file mode 100644 index 09e600fe..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/UserNameTransformRules.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -namespace Multifactor.Radius.Adapter.v2.Core.Auth; - -public class UserNameTransformRules -{ - private readonly UserNameTransformRule[] _firstFactorRules; - public UserNameTransformRule[] BeforeFirstFactor => _firstFactorRules; - - private readonly UserNameTransformRule[] _secondFactorRules; - public UserNameTransformRule[] BeforeSecondFactor => _secondFactorRules; - - public UserNameTransformRules(IEnumerable firstFactorRules, IEnumerable secondFactorRules) - { - Throw.IfNull(firstFactorRules, nameof(firstFactorRules)); - Throw.IfNull(secondFactorRules, nameof(secondFactorRules)); - - _firstFactorRules = firstFactorRules.ToArray(); - _secondFactorRules = secondFactorRules.ToArray(); - } - - public UserNameTransformRules() - { - _firstFactorRules = Array.Empty(); - _secondFactorRules = Array.Empty(); - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/ClientConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/ClientConfigurationFactory.cs deleted file mode 100644 index 1858748b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/ClientConfigurationFactory.cs +++ /dev/null @@ -1,483 +0,0 @@ -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; -using NetTools; -using AppSettingsSection = Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.AppSettingsSection; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; - -public class ClientConfigurationFactory : IClientConfigurationFactory -{ - private readonly IRadiusDictionary _dictionary; - - public ClientConfigurationFactory(IRadiusDictionary dictionary) - { - _dictionary = dictionary; - } - - public IClientConfiguration CreateConfig( - string name, - RadiusAdapterConfiguration configuration, - IServiceConfiguration serviceConfig) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - } - - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(serviceConfig); - - var appSettings = configuration.AppSettings; - ValidateAppSettings(appSettings, name); - - var firstFactorAuthenticationSource = Enum.Parse( - appSettings.FirstFactorAuthenticationSource, - true); - - var appSettingsUrls = Utils.SplitString(appSettings.MultifactorApiUrl); - var mfUrls = serviceConfig.ApiUrls.Count > 0 ? serviceConfig.ApiUrls : appSettingsUrls; - var builder = new ClientConfiguration( - name, - appSettings.RadiusSharedSecret, - firstFactorAuthenticationSource, - appSettings.MultifactorNasIdentifier, - appSettings.MultifactorSharedSecret, - mfUrls); - - builder.SetBypassSecondFactorWhenApiUnreachable(appSettings.BypassSecondFactorWhenApiUnreachable); - - ReadPrivacyModeSetting(appSettings, builder); - ReadInvalidCredDelaySetting(appSettings, builder, serviceConfig); - ReadPreAuthModeSetting(appSettings, builder); - - if (builder.FirstFactorAuthenticationSource == AuthenticationSource.Radius) - ReadRadiusAuthenticationSourceSettings(builder, appSettings); - - ReadLdapServersSettings(builder, configuration.LdapServers); - ReadRadiusReplyAttributes(builder, _dictionary, configuration.RadiusReply); - ReadIpWhiteListSetting(builder, configuration.AppSettings.IpWhiteList); - - LoadUserNameTransformRulesSection(configuration, builder); - - ReadSignUpGroupsSettings(builder, appSettings); - ReadAuthenticationCacheSettings(appSettings, builder); - - var callingStationIdAttr = appSettings.CallingStationIdAttribute; - if (!string.IsNullOrWhiteSpace(callingStationIdAttr)) - { - builder.SetCallingStationIdVendorAttribute(callingStationIdAttr); - } - - Validate(builder); - return builder; - } - - private static void ReadLdapServersSettings(ClientConfiguration builder, LdapServersSection ldapServersSection) - { - if (ldapServersSection.Servers.Length == 0) - return; - - ValidateLdapServers(ldapServersSection, builder.Name); - - foreach (var ldapSettings in ldapServersSection.Servers) - { - var ldapConfig = new LdapServerConfiguration( - ldapSettings.ConnectionString, - ldapSettings.UserName, - ldapSettings.Password); - var settings = new LdapServerInitializeRequest(ldapSettings); - ldapConfig.Initialize(settings); - - builder.AddLdapServers(ldapConfig); - } - } - - private static void ReadInvalidCredDelaySetting( - AppSettingsSection appSettings, - ClientConfiguration builder, - IServiceConfiguration serviceConfig) - { - var credDelay = appSettings.InvalidCredentialDelay; - if (string.IsNullOrWhiteSpace(credDelay)) - { - builder.SetInvalidCredentialDelay(serviceConfig.InvalidCredentialDelay); - return; - } - - try - { - var waiterConfig = RandomWaiterConfig.Create(credDelay); - builder.SetInvalidCredentialDelay(waiterConfig); - } - catch - { - throw InvalidConfigurationException.For( - x => x.AppSettings.InvalidCredentialDelay, - "Can't parse '{prop}' value. Config name: '{0}'", - builder.Name); - } - } - - private static void ReadPreAuthModeSetting(AppSettingsSection appSettings, ClientConfiguration builder) - { - try - { - builder.SetPreAuthMode(PreAuthModeDescriptor.Create(appSettings.PreAuthenticationMethod, PreAuthModeSettings.Default)); - } - catch - { - throw InvalidConfigurationException.For( - x => x.AppSettings.PreAuthenticationMethod, - "Can't parse '{prop}' value. Must be one of: {0}. Config name: '{1}'", - PreAuthModeDescriptor.DisplayAvailableModes(), - builder.Name); - } - - if (builder.PreAuthnMode.Mode != PreAuthMode.None && builder.InvalidCredentialDelay.Min < 2) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.InvalidCredentialDelay, - "To enable pre-auth second factor for this client please set '{prop}' min value to 2 or more. Config name: '{0}'", - builder.Name); - } - } - - private static void ReadPrivacyModeSetting(AppSettingsSection appSettings, ClientConfiguration builder) - { - try - { - builder.SetPrivacyMode(PrivacyModeDescriptor.Create(appSettings.PrivacyMode)); - } - catch - { - throw InvalidConfigurationException.For( - x => x.AppSettings.PrivacyMode, - "Can't parse '{prop}' value. Must be one of: Full, None, Partial:Field1,Field2. Config name: '{0}'", - builder.Name); - } - } - - private static void LoadUserNameTransformRulesSection(RadiusAdapterConfiguration configuration, ClientConfiguration builder) - { - var userNameTransformRulesSection = configuration.UserNameTransformRules; - var firstFactorRules = new List(); - var secondFactorRules = new List(); - - if (userNameTransformRulesSection?.Elements?.Length > 0) - { - firstFactorRules.AddRange(userNameTransformRulesSection.Elements); - secondFactorRules.AddRange(userNameTransformRulesSection.Elements); - } - - firstFactorRules.AddRange(userNameTransformRulesSection?.BeforeFirstFactor?.Elements ?? []); - secondFactorRules.AddRange(userNameTransformRulesSection?.BeforeSecondFactor?.Elements ?? []); - - builder.SetUserNameTransformRules( - new UserNameTransformRules(firstFactorRules, secondFactorRules) - ); - } - - private static void ReadRadiusAuthenticationSourceSettings(ClientConfiguration builder, AppSettingsSection appSettings) - { - if (string.IsNullOrWhiteSpace(appSettings.AdapterClientEndpoint)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.AdapterClientEndpoint, - "'{prop}' element not found. Config name: '{0}'", - builder.Name); - } - - if (string.IsNullOrWhiteSpace(appSettings.NpsServerEndpoint)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.NpsServerEndpoint, - "'{prop}' element not found. Config name: '{0}'", - builder.Name); - } - - if (!IPEndPointFactory.TryParse(appSettings.AdapterClientEndpoint, out var serviceClientEndpoint)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.AdapterClientEndpoint, - "Can't parse '{prop}' value. Config name: '{0}'", - builder.Name); - } - - var npsServers = Utils.SplitString(appSettings.NpsServerEndpoint); - foreach (var server in npsServers) - { - if (!IPEndPointFactory.TryParse(server, out var npsEndpoint)) - throw new InvalidConfigurationException($"Config name: '{builder.Name}'. Invalid NPS server endpoint: '{server}'"); - - builder.AddNpsServerEndpoint(npsEndpoint); - } - - var timeoutStr = appSettings.NpsServerTimeout; - if (!string.IsNullOrWhiteSpace(timeoutStr)) - { - if (TimeSpan.TryParseExact(timeoutStr, @"hh\:mm\:ss", null, TimeSpanStyles.None, out var npsServerTimeout)) - { - builder.SetNpsServerTimeout(npsServerTimeout); - } - else - { - throw new InvalidConfigurationException($"Config name: '{builder.Name}'. Invalid NPS server timeout: '{timeoutStr}'"); - } - } - - builder.SetServiceClientEndpoint(serviceClientEndpoint); - } - - private static void ReadSignUpGroupsSettings(ClientConfiguration builder, AppSettingsSection appSettings) - { - const string signUpGroupsRegex = @"([\wа-я\s\-]+)(\s*;\s*([\wа-я\s\-]+)*)*"; - - var signUpGroupsSettings = appSettings.SignUpGroups; - if (string.IsNullOrWhiteSpace(signUpGroupsSettings)) - { - builder.SetSignUpGroups(string.Empty); - return; - } - - if (!Regex.IsMatch(signUpGroupsSettings, signUpGroupsRegex, RegexOptions.IgnoreCase)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.SignUpGroups, - "Invalid group names. Please check '{prop}' settings property and fix syntax errors. Config name: '{0}'", - builder.Name); - } - - builder.SetSignUpGroups(signUpGroupsSettings); - } - - private static void ReadAuthenticationCacheSettings(AppSettingsSection appSettings, ClientConfiguration builder) - { - try - { - var ltConf = AuthenticatedClientCacheConfig.Create(appSettings.AuthenticationCacheLifetime); - builder.SetAuthenticationCacheLifetime(ltConf); - } - catch - { - throw InvalidConfigurationException.For( - x => x.AppSettings.AuthenticationCacheLifetime, - "Can't parse '{prop}' value. Config name: '{0}'", - builder.Name); - } - } - - private static void ReadRadiusReplyAttributes( - ClientConfiguration builder, - IRadiusDictionary dictionary, - RadiusReplySection? radiusReplyAttributesSection) - { - var replyAttributes = new Dictionary>(); - - if (radiusReplyAttributesSection != null) - { - foreach (var attribute in radiusReplyAttributesSection.Attributes.Elements) - { - var radiusAttribute = dictionary.GetAttribute(attribute.Name) ?? - throw new InvalidConfigurationException( - $"Unknown attribute '{attribute.Name}' in RadiusReply configuration element, please see dictionary. Config name: '{builder.Name}'"); - - if (!replyAttributes.ContainsKey(attribute.Name)) - replyAttributes.Add(attribute.Name, new List()); - - if (!string.IsNullOrWhiteSpace(attribute.From)) - { - replyAttributes[attribute.Name] - .Add(new RadiusReplyAttributeValue(attribute.From, attribute.Sufficient)); - continue; - } - - try - { - var value = ParseRadiusReplyAttributeValue(radiusAttribute, attribute.Value); - replyAttributes[attribute.Name] - .Add(new RadiusReplyAttributeValue(value, attribute.When, attribute.Sufficient)); - } - catch (Exception ex) - { - throw new InvalidConfigurationException( - $"Error while parsing attribute '{radiusAttribute.Name}' with {radiusAttribute.Type} value '{attribute.Value}' in RadiusReply configuration element: {ex.Message}. Config name: '{builder.Name}'"); - } - } - } - - foreach (var attr in replyAttributes) - { - builder.AddRadiusReplyAttribute(attr.Key, attr.Value); - } - } - - private static object ParseRadiusReplyAttributeValue(DictionaryAttribute attribute, string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - throw new Exception("Value must be specified"); - } - - return attribute.Type switch - { - DictionaryAttribute.TypeString or DictionaryAttribute.TypeTaggedString => value, - DictionaryAttribute.TypeInteger or DictionaryAttribute.TypeTaggedInteger => uint.Parse(value), - DictionaryAttribute.TypeIpAddr => IPAddress.Parse(value), - DictionaryAttribute.TypeOctet => Utils.StringToByteArray(value), - _ => throw new Exception($"Unknown type {attribute.Type}") - }; - } - - private static void ReadIpWhiteListSetting(ClientConfiguration builder, string ipWhiteList) - { - var splittedRanges = Utils.SplitString(ipWhiteList); - - foreach (var range in splittedRanges) - { - if (!IPAddressRange.TryParse(range, out var ipAddressRange)) - throw new InvalidConfigurationException($"Invalid IP address range: '{range}' in '{builder.Name}' config"); - builder.AddWhiteIpRange(ipAddressRange); - } - } - - private void ValidateAppSettings(AppSettingsSection appSettings, string configName) - { - if (string.IsNullOrWhiteSpace(appSettings.FirstFactorAuthenticationSource)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.FirstFactorAuthenticationSource, - "'{prop}' element not found. Config name: '{0}'", - configName); - } - - var isDigit = int.TryParse(appSettings.FirstFactorAuthenticationSource, out _); - var isValidAuthSource = - Enum.TryParse(appSettings.FirstFactorAuthenticationSource, true, out _); - var authTypes = Enum.GetNames(); - - if (isDigit || !isValidAuthSource) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.FirstFactorAuthenticationSource, - "Can't parse '{prop}' value. Must be one of: {1}. Config name: '{0}'", - configName, - string.Join(", ", authTypes)); - } - - if (string.IsNullOrWhiteSpace(appSettings.RadiusSharedSecret)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.RadiusSharedSecret, - "'{prop}' element not found. Config name: '{0}'", - configName); - } - - if (string.IsNullOrWhiteSpace(appSettings.MultifactorNasIdentifier)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.MultifactorNasIdentifier, - "'{prop}' element not found. Config name: '{0}'", - configName); - } - - if (string.IsNullOrWhiteSpace(appSettings.MultifactorSharedSecret)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.MultifactorSharedSecret, - "'{prop}' element not found. Config name: '{0}'", - configName); - } - } - - private static void ValidateLdapServers(LdapServersSection section, string configName) - { - foreach (var server in section.Servers) - { - if (string.IsNullOrWhiteSpace(server.ConnectionString)) - { - throw InvalidConfigurationException.For( - x => server.ConnectionString, - "Can't parse '{prop}' value. Config name: '{0}'", - configName); - } - - if (string.IsNullOrWhiteSpace(server.Password)) - { - throw InvalidConfigurationException.For( - x => server.Password, - "Can't parse '{prop}' value. Config name: '{0}'", - configName); - } - - if (string.IsNullOrWhiteSpace(server.UserName)) - { - throw InvalidConfigurationException.For( - x => server.UserName, - "Can't parse '{prop}' value. Config name: '{0}'", - configName); - } - - var serverName = server.ConnectionString; - if (server is { EnableTrustedDomains: true, RequiresUpn: false }) - throw new InvalidConfigurationException($"Config name: '{configName}', LDAP server: '{serverName}'. To use trusted domains also set 'requires-upn' to 'true'."); - - if (!string.IsNullOrWhiteSpace(server.IncludedDomains) && !string.IsNullOrWhiteSpace(server.ExcludedDomains)) - throw new InvalidConfigurationException($"Config name: '{configName}', LDAP server: '{serverName}'. Simultaneous use of 'included-domains' and 'excluded-domains' is not allowed."); - - if (!string.IsNullOrWhiteSpace(server.IncludedSuffixes) && !string.IsNullOrWhiteSpace(server.ExcludedSuffixes)) - throw new InvalidConfigurationException($"Config name: '{configName}', LDAP server: '{serverName}'. Simultaneous use of 'included-suffixes' and 'excluded-suffixes' is not allowed."); - - ValidateDnFormat(configName, serverName, Utils.SplitString(server.AccessGroups)); - ValidateDnFormat(configName, serverName, Utils.SplitString(server.AuthenticationCacheGroups)); - ValidateDnFormat(configName, serverName, Utils.SplitString(server.SecondFaGroups)); - ValidateDnFormat(configName, serverName, Utils.SplitString(server.SecondFaBypassGroups)); - ValidateDnFormat(configName, serverName, Utils.SplitString(server.NestedGroupsBaseDn)); - } - } - - private void Validate(ClientConfiguration builder) - { - var serversRequired = IsLdapServerRequired(builder); - if (serversRequired && builder.LdapServers.Count == 0) - throw InvalidConfigurationException.For( - x => x.LdapServers, - "Can't parse '{prop}' value. Config name: '{0}'", - builder.Name); - } - - private static bool IsLdapServerRequired(ClientConfiguration builder) - { - var ldapFirstFactor = builder.FirstFactorAuthenticationSource == AuthenticationSource.Ldap; - var hasReplyAttributesFromLdap = builder.RadiusReplyAttributes.Values.SelectMany(x => x).Any(x => x.FromLdap || x.IsMemberOf || x.UserGroupCondition.Count > 0); - return ldapFirstFactor || hasReplyAttributesFromLdap; - } - - private static void ValidateDnFormat(string configName, string serverName, IEnumerable groups) - { - foreach (var group in groups) - { - try - { - new DistinguishedName(group); - } - catch (ArgumentException) - { - throw new InvalidConfigurationException($"Config name: '{configName}', LDAP server: '{serverName}'. Invalid format: {group}. Distinguished name is required."); - } - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/DefaultClientConfigurationsProvider.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/DefaultClientConfigurationsProvider.cs deleted file mode 100644 index 1c1e39ef..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/DefaultClientConfigurationsProvider.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; - -public class DefaultClientConfigurationsProvider : IClientConfigurationsProvider -{ - private readonly Lazy> _loaded; - private readonly ApplicationVariables _variables; - private readonly ILogger _logger; - - public DefaultClientConfigurationsProvider(ApplicationVariables variables, - ILogger logger) - { - _variables = variables ?? throw new ArgumentNullException(nameof(variables)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loaded = new Lazy>(Load); - } - - public RadiusAdapterConfiguration[] GetClientConfigurations() => _loaded.Value.Select(x => x.Value).ToArray(); - - public RadiusConfigurationSource GetSource(RadiusAdapterConfiguration configuration) - { - var pair = _loaded.Value.FirstOrDefault(x => x.Value == configuration); - // default (KeyValuePair) is KeyValuePair - return pair.Key; - } - - private Dictionary Load() - { - var clientConfigFilesPath = $"{_variables.AppPath}{Path.DirectorySeparatorChar}clients"; - var clientConfigFiles = Directory.Exists(clientConfigFilesPath) - ? Directory.GetFiles(clientConfigFilesPath, "*.config") - : Array.Empty(); - - var dict = new Dictionary(); - - var fileSources = clientConfigFiles.Select(x => new RadiusConfigurationFile(x)).ToArray(); - foreach (var file in fileSources) - { - _logger.LogInformation("Loading client configuration from {path:l}", file); - - var config = RadiusAdapterConfigurationFactory.Create(file, file.Name); - dict.Add(file, config); - } - - var envVarSources = GetEnvVarClients() - .Select(x => new RadiusConfigurationEnvironmentVariable(x)) - .ExceptBy(fileSources.Select(x => RadiusConfigurationSource.TransformName(x.Name)), x => x.Name); - foreach (var envVarClient in envVarSources) - { - _logger.LogInformation("Found environment variable client '{Client:l}'", envVarClient); - - var config = RadiusAdapterConfigurationFactory.Create(envVarClient); - dict.Add(envVarClient, config); - } - - return dict; - } - - internal static IEnumerable GetEnvVarClients() - { - var patterns = RadiusAdapterConfiguration.KnownSectionNames - .Select(x => $"^(?i){ConfigurationBuilderExtensions.BasePrefix}(?[a-zA-Z_]+[a-zA-Z0-9_]*)_{x}") - .ToArray(); - - var keys = Environment.GetEnvironmentVariables().Keys - .Cast() - .Where(x => x.StartsWith(ConfigurationBuilderExtensions.BasePrefix, StringComparison.OrdinalIgnoreCase)); - - foreach (var key in keys) - { - var groupCollection = patterns.Select(x => Regex.Match(key, x).Groups).FirstOrDefault(x => x.Count != 0); - if (groupCollection is null) - { - continue; - } - - if (!groupCollection.TryGetValue("cli", out var cli)) - { - continue; - } - - yield return cli.Value; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationFactory.cs deleted file mode 100644 index 22859206..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; - -public interface IClientConfigurationFactory -{ - IClientConfiguration CreateConfig(string name, RadiusAdapterConfiguration configuration, IServiceConfiguration serviceConfig); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationsProvider.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationsProvider.cs deleted file mode 100644 index 782d2efe..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationsProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; - -/// -/// Provides Radius Adapter client configurations (in multi-client mode). -/// -public interface IClientConfigurationsProvider -{ - /// - /// Returns a config descriptor from which the specified configuration was read. - /// - /// Configuration instance. - /// Radius Configuration File - RadiusConfigurationSource GetSource(RadiusAdapterConfiguration configuration); - - /// - /// Returns all client configurations. - /// - /// - RadiusAdapterConfiguration[] GetClientConfigurations(); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ClientConfiguration.cs deleted file mode 100644 index 64ba8673..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ClientConfiguration.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public class ClientConfiguration : IClientConfiguration -{ - private readonly List _ldapServers = new(); - private readonly List _ipWhiteList = new(); - private readonly HashSet _npsServers = new(); - - public IReadOnlyList LdapServers => _ldapServers; - public IReadOnlyList ApiUrls { get; } - - public ClientConfiguration(string name, - string rdsSharedSecret, - AuthenticationSource firstFactorAuthSource, - string apiKey, - string apiSecret, - IEnumerable apiUrls) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); - - if (string.IsNullOrWhiteSpace(rdsSharedSecret)) - throw new ArgumentException($"'{nameof(rdsSharedSecret)}' cannot be null or whitespace.", - nameof(rdsSharedSecret)); - - if (string.IsNullOrWhiteSpace(apiKey)) - throw new ArgumentException($"'{nameof(apiKey)}' cannot be null or whitespace.", nameof(apiKey)); - - if (string.IsNullOrWhiteSpace(apiSecret)) - throw new ArgumentException($"'{nameof(apiSecret)}' cannot be null or whitespace.", nameof(apiSecret)); - - var urls = apiUrls?.ToList(); - if (urls is null || urls.Count == 0) - throw new ArgumentException($"'{nameof(apiUrls)}' cannot be null or empty.", nameof(apiUrls)); - - BypassSecondFactorWhenApiUnreachable = true; //by default - - Name = name; - RadiusSharedSecret = rdsSharedSecret; - FirstFactorAuthenticationSource = firstFactorAuthSource; - ApiCredential = new ApiCredential(apiKey, apiSecret); - ApiUrls = urls; - } - - /// - /// Friendly client name - /// - public string Name { get; } - - /// - /// Shared secret between this service and Radius client - /// - public string RadiusSharedSecret { get; } - - /// - /// Where to handle first factor (UserName and Password) - /// - public AuthenticationSource FirstFactorAuthenticationSource { get; } - - public ApiCredential ApiCredential { get; } - - /// - /// Bypass second factor when MultiFactor API is unreachable - /// - public bool BypassSecondFactorWhenApiUnreachable { get; private set; } - - public TimeSpan NpsServerTimeout { get; private set; } = TimeSpan.FromSeconds(5); - - public PrivacyModeDescriptor PrivacyMode { get; private set; } = PrivacyModeDescriptor.Default; - - /// - /// This service RADIUS UDP Client endpoint - /// - public IPEndPoint ServiceClientEndpoint { get; private set; } - - /// - /// Network Policy Service RADIUS UDP Server endpoint - /// - public IReadOnlySet NpsServerEndpoints => _npsServers; - - /// - /// Groups to assign to the registered user.Specified groups will be assigned to a new user. - /// Syntax: group names (from your Management Portal) separated by semicolons. - /// - /// Example: group1;Group Name Two; - /// - /// - public string SignUpGroups { get; private set; } - - public AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; private set; } = - AuthenticatedClientCacheConfig.Default; - - private readonly Dictionary _radiusReplyAttributes = new(); - - /// - /// Custom RADIUS reply attributes - /// - public IReadOnlyDictionary RadiusReplyAttributes => _radiusReplyAttributes; - - /// - /// Username transform rules - /// - public UserNameTransformRules UserNameTransformRules { get; private set; } = new(); - - public ClientConfiguration SetUserNameTransformRules(UserNameTransformRules val) - { - UserNameTransformRules = val; - return this; - } - - public string CallingStationIdVendorAttribute { get; private set; } - - public RandomWaiterConfig InvalidCredentialDelay { get; private set; } - public PreAuthModeDescriptor PreAuthnMode { get; private set; } = PreAuthModeDescriptor.Default; - public IReadOnlyList IpWhiteList => _ipWhiteList; - - public ClientConfiguration SetBypassSecondFactorWhenApiUnreachable(bool val) - { - BypassSecondFactorWhenApiUnreachable = val; - return this; - } - - public ClientConfiguration SetPrivacyMode(PrivacyModeDescriptor val) - { - PrivacyMode = val; - return this; - } - - public ClientConfiguration SetServiceClientEndpoint(IPEndPoint val) - { - ServiceClientEndpoint = val; - return this; - } - - public ClientConfiguration AddNpsServerEndpoint(IPEndPoint val) - { - ArgumentNullException.ThrowIfNull(val); - _npsServers.Add(val); - return this; - } - - public ClientConfiguration SetNpsServerTimeout(TimeSpan val) - { - if (val.TotalMilliseconds <= 0) - throw new ArgumentException($"Invalid NPS server timeout: {val}"); - - NpsServerTimeout = val; - return this; - } - - public ClientConfiguration SetSignUpGroups(string val) - { - SignUpGroups = val; - return this; - } - - public ClientConfiguration SetAuthenticationCacheLifetime(AuthenticatedClientCacheConfig val) - { - AuthenticationCacheLifetime = val; - return this; - } - - public ClientConfiguration AddRadiusReplyAttribute(string attr, IEnumerable values) - { - if (string.IsNullOrWhiteSpace(attr)) - throw new ArgumentException($"'{nameof(attr)}' cannot be null or whitespace.", nameof(attr)); - - if (values is null) - throw new ArgumentNullException(nameof(values)); - - _radiusReplyAttributes[attr] = values.ToArray(); - return this; - } - - public ClientConfiguration SetCallingStationIdVendorAttribute(string val) - { - if (string.IsNullOrWhiteSpace(val)) - { - throw new ArgumentException($"'{nameof(val)}' cannot be null or whitespace.", nameof(val)); - } - - CallingStationIdVendorAttribute = val; - return this; - } - - public ClientConfiguration SetInvalidCredentialDelay(RandomWaiterConfig val) - { - InvalidCredentialDelay = val ?? throw new ArgumentNullException(nameof(val)); - return this; - } - - public ClientConfiguration SetPreAuthMode(PreAuthModeDescriptor val) - { - PreAuthnMode = val ?? throw new ArgumentNullException(nameof(val)); - return this; - } - - public ClientConfiguration AddLdapServers(params ILdapServerConfiguration[] ldapServers) - { - if (ldapServers?.Length > 0) - _ldapServers.AddRange(ldapServers); - else - throw new ArgumentNullException(nameof(ldapServers)); - return this; - } - - public ClientConfiguration AddWhiteIpRange(IPAddressRange range) - { - ArgumentNullException.ThrowIfNull(range); - _ipWhiteList.Add(range); - return this; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IClientConfiguration.cs deleted file mode 100644 index b2c92cde..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IClientConfiguration.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public interface IClientConfiguration -{ - IReadOnlyList LdapServers { get; } - AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; } - bool BypassSecondFactorWhenApiUnreachable { get; } - string CallingStationIdVendorAttribute { get; } - AuthenticationSource FirstFactorAuthenticationSource { get; } - ApiCredential ApiCredential { get; } - string Name { get; } - IReadOnlySet NpsServerEndpoints { get; } - TimeSpan NpsServerTimeout { get; } - PrivacyModeDescriptor PrivacyMode { get; } - IReadOnlyDictionary RadiusReplyAttributes { get; } - string RadiusSharedSecret { get; } - IPEndPoint ServiceClientEndpoint { get; } - string SignUpGroups { get; } - UserNameTransformRules UserNameTransformRules { get; } - RandomWaiterConfig InvalidCredentialDelay { get; } - PreAuthModeDescriptor PreAuthnMode { get; } - IReadOnlyList IpWhiteList { get; } - IReadOnlyList ApiUrls { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ILdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ILdapServerConfiguration.cs deleted file mode 100644 index 11fa0270..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ILdapServerConfiguration.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public interface ILdapServerConfiguration -{ - string ConnectionString { get; } - string UserName { get; } - string Password { get; } - int BindTimeoutInSeconds { get; } - bool LoadNestedGroups { get; } - string? IdentityAttribute { get; } - IReadOnlyList AccessGroups { get; } - IReadOnlyList SecondFaGroups { get; } - IReadOnlyList SecondFaBypassGroups { get; } - IReadOnlyList NestedGroupsBaseDns { get; } - IReadOnlyList PhoneAttributes { get; } - IReadOnlyList IpWhiteList { get; } - IReadOnlyList AuthenticationCacheGroups { get; } - int LdapSchemaCacheLifeTimeInHours { get; } - int UserProfileCacheLifeTimeInHours { get; } - IPermissionRules DomainPermissions { get; } - IPermissionRules SuffixesPermissions { get; } - bool TrustedDomainsEnabled { get; } - bool AlternativeSuffixesEnabled { get; } - bool UpnRequired { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IPermissionRules.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IPermissionRules.cs deleted file mode 100644 index 401aa0bb..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IPermissionRules.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public interface IPermissionRules -{ - bool IsPermitted(string domain); - List IncludedValues { get; } - List ExcludedValues { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerConfiguration.cs deleted file mode 100644 index 825b0cdc..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerConfiguration.cs +++ /dev/null @@ -1,205 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public class LdapServerConfiguration : ILdapServerConfiguration -{ - private string? _identity; - private bool _loadNestedGroups; - private int _timeout; - private bool _trustedDomainsEnabled; - private bool _alternativeSuffixesEnabled; - private bool _requiresUpn; - private readonly List _accessGroups = new(); - private readonly List _2FaGroups = new(); - private readonly List _2FaBypassGroups = new(); - private readonly List _baseDns = new(); - private readonly List _phones = new(); - private IPermissionRules _domainPermissionRules = new PermissionRules(); - private IPermissionRules _suffixesPermissionRules = new PermissionRules(); - private readonly List _ipWhiteList = new(); - private readonly List _authenticationCacheGroups = new(); - - public string ConnectionString { get; } - public string UserName { get; } - public string Password { get; } - public int BindTimeoutInSeconds => _timeout; - public bool LoadNestedGroups => _loadNestedGroups; - public string? IdentityAttribute => _identity; - public IReadOnlyList AccessGroups => _accessGroups; - public IReadOnlyList SecondFaGroups => _2FaGroups; - public IReadOnlyList SecondFaBypassGroups => _2FaBypassGroups; - public IReadOnlyList NestedGroupsBaseDns => _baseDns; - public IReadOnlyList PhoneAttributes => _phones; - public IPermissionRules DomainPermissions => _domainPermissionRules; - public IPermissionRules SuffixesPermissions => _suffixesPermissionRules; - public int LdapSchemaCacheLifeTimeInHours { get; } = 1; - public int UserProfileCacheLifeTimeInHours { get; } = 0; - public bool TrustedDomainsEnabled => _trustedDomainsEnabled; - public bool AlternativeSuffixesEnabled => _alternativeSuffixesEnabled; - public bool UpnRequired => _requiresUpn; - public IReadOnlyList IpWhiteList => _ipWhiteList; - public IReadOnlyList AuthenticationCacheGroups => _authenticationCacheGroups; - - public LdapServerConfiguration(string connectionString, string userName, string password) - { - ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); - ArgumentException.ThrowIfNullOrWhiteSpace(userName); - ArgumentException.ThrowIfNullOrWhiteSpace(password); - - ConnectionString = connectionString; - UserName = userName; - Password = password; - } - - public void Initialize(LdapServerInitializeRequest settings) - { - AddPhoneAttributes(settings.PhoneAttributes) - .AddAccessGroups(settings.AccessGroups.Select(x=> x.StringRepresentation)) - .AddSecondFaGroups(settings.SecondFaGroups.Select(x=> x.StringRepresentation)) - .AddSecondFaBypassGroups(settings.SecondFaBypassGroups.Select(x=> x.StringRepresentation)) - .AddNestedGroupBaseDns(settings.NestedGroupsBaseDns.Select(x=> x.StringRepresentation)) - .SetIdentityAttribute(settings.IdentityAttribute) - .SetLoadNestedGroups(settings.LoadNestedGroups) - .SetBindTimeoutInSeconds(settings.BindTimeoutInSeconds) - .RequiresUpn(settings.RequiresUpn) - .EnableTrustedDomains(settings.EnableTrustedDomains) - .EnableAlternativeSuffixes(settings.EnableAlternativeSuffixes) - .SetDomainRules(settings.DomainPermissions) - .SetAlternativeSuffixesRules(settings.SuffixesPermissions) - .AddAuthenticationCacheGroups(settings.AuthenticationCacheGroups.Select(x=> x.StringRepresentation)); - } - - public LdapServerConfiguration EnableTrustedDomains(bool enable = true) - { - _trustedDomainsEnabled = enable; - return this; - } - - public LdapServerConfiguration EnableAlternativeSuffixes(bool enable = true) - { - _alternativeSuffixesEnabled = enable; - return this; - } - - public LdapServerConfiguration RequiresUpn(bool requires = true) - { - _requiresUpn = requires; - return this; - } - - public LdapServerConfiguration SetDomainRules(IPermissionRules rules) - { - _domainPermissionRules = rules; - return this; - } - - public LdapServerConfiguration SetAlternativeSuffixesRules(IPermissionRules rules) - { - _suffixesPermissionRules = rules; - return this; - } - - public LdapServerConfiguration SetBindTimeoutInSeconds(int seconds) - { - if (seconds <= 0) - throw new ArgumentOutOfRangeException(nameof(seconds)); - - _timeout = seconds; - return this; - } - - public LdapServerConfiguration SetLoadNestedGroups(bool shouldLoad) - { - _loadNestedGroups = shouldLoad; - return this; - } - - public LdapServerConfiguration SetIdentityAttribute(string? attributeName) - { - _identity = attributeName; - return this; - } - - public LdapServerConfiguration AddAccessGroups(IEnumerable groups) - { - if (groups is null) - return this; - - AddToList(_accessGroups, groups.Select(x => new DistinguishedName(x))); - return this; - } - - public LdapServerConfiguration AddSecondFaGroups(IEnumerable groups) - { - if (groups is null) - return this; - - AddToList(_2FaGroups, groups.Select(x => new DistinguishedName(x))); - return this; - } - - public LdapServerConfiguration AddSecondFaBypassGroups(IEnumerable groups) - { - if (groups is null) - return this; - - AddToList(_2FaBypassGroups, groups.Select(x => new DistinguishedName(x))); - return this; - } - - public LdapServerConfiguration AddNestedGroupBaseDns(IEnumerable items) - { - if (items is null) - return this; - - AddToList(_baseDns, items.Select(x => new DistinguishedName(x))); - return this; - } - - public LdapServerConfiguration AddPhoneAttributes(IEnumerable items) - { - if (items is null) - return this; - - AddToList(_phones, items); - return this; - } - - //maybe for future - public LdapServerConfiguration AddWhiteIpList(IEnumerable ranges) - { - if (ranges is null) - return this; - - foreach (var range in ranges) - { - if (!IPAddressRange.TryParse(range, out var ipAddressRange)) - throw new InvalidConfigurationException($"Invalid IP address range: '{range}' config"); - - AddToList(_ipWhiteList, [ipAddressRange]); - } - - return this; - } - - public LdapServerConfiguration AddAuthenticationCacheGroups(IEnumerable groups) - { - if (groups != null) - return AddToList(_authenticationCacheGroups, groups.Select(x => new DistinguishedName(x))); - return this; - } - - private LdapServerConfiguration AddToList(IList target, IEnumerable items) - { - foreach (var item in items) - { - if (!target.Contains(item)) - target.Add(item); - } - - return this; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerInitializeRequest.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerInitializeRequest.cs deleted file mode 100644 index 50fcc94e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerInitializeRequest.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Multifactor.Core.Ldap.Name; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public class LdapServerInitializeRequest -{ - public IEnumerable PhoneAttributes { get; set; } = Array.Empty(); - public IEnumerable AccessGroups { get; set; } = Array.Empty(); - public IEnumerable SecondFaGroups { get; set; } = Array.Empty(); - public IEnumerable SecondFaBypassGroups { get; set; } = Array.Empty(); - public IEnumerable NestedGroupsBaseDns { get; set; } = Array.Empty(); - public IEnumerable AuthenticationCacheGroups { get; set; } = Array.Empty(); - public string? IdentityAttribute { get; set; } = string.Empty; - public bool LoadNestedGroups { get; set; } = true; - public int BindTimeoutInSeconds { get; set; } = 30; - public bool RequiresUpn { get; set; } = false; - public bool EnableTrustedDomains { get; set; } = false; - public bool EnableAlternativeSuffixes { get; set; } = false; - public IPermissionRules DomainPermissions { get; set; } = new PermissionRules(); - public IPermissionRules SuffixesPermissions { get; set; } = new PermissionRules(); - - public LdapServerInitializeRequest() - { - } - - public LdapServerInitializeRequest(ILdapServerConfiguration config) - { - PhoneAttributes = config.PhoneAttributes; - AccessGroups = config.AccessGroups; - SecondFaGroups = config.SecondFaGroups; - SecondFaBypassGroups = config.SecondFaBypassGroups; - NestedGroupsBaseDns = config.NestedGroupsBaseDns; - IdentityAttribute = config.IdentityAttribute; - LoadNestedGroups = config.LoadNestedGroups; - BindTimeoutInSeconds = config.BindTimeoutInSeconds; - RequiresUpn = config.UpnRequired; - EnableTrustedDomains = config.TrustedDomainsEnabled; - EnableAlternativeSuffixes = config.AlternativeSuffixesEnabled; - DomainPermissions = config.DomainPermissions; - SuffixesPermissions = config.SuffixesPermissions; - AuthenticationCacheGroups = config.AuthenticationCacheGroups; - } - - public LdapServerInitializeRequest(Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer.LdapServerConfiguration config) - { - PhoneAttributes = Split(config.PhoneAttributes); - AccessGroups = Split(config.AccessGroups).Select(x => new DistinguishedName(x)); - SecondFaGroups = Split(config.SecondFaGroups).Select(x => new DistinguishedName(x)); - SecondFaBypassGroups = Split(config.SecondFaBypassGroups).Select(x => new DistinguishedName(x)); - NestedGroupsBaseDns = Split(config.NestedGroupsBaseDn).Select(x => new DistinguishedName(x)); - AuthenticationCacheGroups = Split(config.AuthenticationCacheGroups).Select(x => new DistinguishedName(x)); - IdentityAttribute = config.IdentityAttribute; - LoadNestedGroups = config.LoadNestedGroups; - BindTimeoutInSeconds = config.BindTimeoutInSeconds; - RequiresUpn = config.RequiresUpn; - EnableTrustedDomains = config.EnableTrustedDomains; - EnableAlternativeSuffixes = config.EnableAlternativeSuffixes; - DomainPermissions = GetPermissionRules( - Split(config.IncludedDomains).ToList(), - Split(config.ExcludedDomains).ToList()); - SuffixesPermissions = GetPermissionRules( - Split(config.IncludedSuffixes).ToList(), - Split(config.ExcludedSuffixes).ToList()); - } - - private static IEnumerable Split(string value) => Utils.SplitString(value.ToLower()); - private static PermissionRules GetPermissionRules(List included, List excluded) => new(included, excluded); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/PermissionRules.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/PermissionRules.cs deleted file mode 100644 index 958c6411..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/PermissionRules.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Multifactor.Core.Ldap.LangFeatures; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -public class PermissionRules : IPermissionRules -{ - /// - /// Allowed values - /// - public List IncludedValues { get; } - - /// - /// Disallowed values - /// - public List ExcludedValues { get; } - - public PermissionRules(List includedDomains, List excludedDomains) - { - Throw.IfNull(includedDomains, nameof(includedDomains)); - Throw.IfNull(excludedDomains, nameof(excludedDomains)); - - IncludedValues = includedDomains; - ExcludedValues = excludedDomains; - } - - public PermissionRules() - { - IncludedValues = new List(); - ExcludedValues = new List(); - } - - public bool IsPermitted(string domain) - { - if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain)); - - if (IncludedValues.Count > 0) - return IncludedValues.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); - - if (ExcludedValues.Count > 0) - return ExcludedValues.All(excluded => !excluded.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); - - return true; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/RadiusReplyAttributeValue.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/RadiusReplyAttributeValue.cs deleted file mode 100644 index b6c0cfb6..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/RadiusReplyAttributeValue.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -/// -/// Radius Access-Accept message extra element -/// -public class RadiusReplyAttributeValue -{ - public bool FromLdap { get; } - - /// - /// Attribute Value - /// - public object Value { get; } - - public bool Sufficient { get; } - - /// - /// Ldap attr name to proxy value from - /// - public string LdapAttributeName { get; } - - /// - /// Is list of all user groups attribute - /// - public bool IsMemberOf => LdapAttributeName?.ToLower() == "memberof"; - - private readonly List _userGroupCondition = new(); - /// - /// User group condition - /// - public IReadOnlyList UserGroupCondition => _userGroupCondition; - - private readonly List _userNameCondition = new(); - - /// - /// User name condition - /// - public IReadOnlyList UserNameCondition => _userNameCondition; - - /// - /// Const value with optional condition - /// - public RadiusReplyAttributeValue(object value, string conditionClause, bool sufficient = false) - { - Value = value; - if (!string.IsNullOrWhiteSpace(conditionClause)) - ParseConditionClause(conditionClause); - Sufficient = sufficient; - } - - /// - /// Proxy value from LDAP attr - /// - public RadiusReplyAttributeValue(string ldapAttributeName, bool sufficient = false) - { - ArgumentException.ThrowIfNullOrWhiteSpace(ldapAttributeName); - - LdapAttributeName = ldapAttributeName; - FromLdap = true; - Sufficient = sufficient; - } - - private void ParseConditionClause(string clause) - { - var parts = clause.Split(['='], StringSplitOptions.RemoveEmptyEntries); - - switch (parts[0]) - { - case "UserGroup": - _userGroupCondition.AddRange(parts[1].Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())); - break; - - case "UserName": - _userNameCondition.AddRange(parts[1].Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())); - break; - - default: - throw new Exception($"Unknown condition '{clause}'"); - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/IServiceConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/IServiceConfigurationFactory.cs deleted file mode 100644 index c3d5ff47..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/IServiceConfigurationFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; - -public interface IServiceConfigurationFactory -{ - IServiceConfiguration CreateConfig(RadiusAdapterConfiguration rootConfiguration); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/ServiceConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/ServiceConfigurationFactory.cs deleted file mode 100644 index 9201a09c..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/ServiceConfigurationFactory.cs +++ /dev/null @@ -1,201 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.Net; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; - -public class ServiceConfigurationFactory : IServiceConfigurationFactory -{ - private readonly IClientConfigurationsProvider _clientConfigurationsProvider; - private readonly IClientConfigurationFactory _clientConfigFactoryLdapSettings; - private readonly ILogger _logger; - private static readonly TimeSpan RecommendedMinimalApiTimeout = TimeSpan.FromSeconds(65); - - public ServiceConfigurationFactory( - IClientConfigurationsProvider clientConfigurationsProvider, - IClientConfigurationFactory clientConfigFactoryLdapSettings, - ILogger logger) - { - _clientConfigurationsProvider = clientConfigurationsProvider ?? throw new ArgumentNullException(nameof(clientConfigurationsProvider)); - _clientConfigFactoryLdapSettings = clientConfigFactoryLdapSettings ?? throw new ArgumentNullException(nameof(clientConfigFactoryLdapSettings)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public IServiceConfiguration CreateConfig(RadiusAdapterConfiguration rootConfiguration) - { - if (rootConfiguration is null) - { - throw new ArgumentNullException(nameof(rootConfiguration)); - } - - var appSettings = rootConfiguration.AppSettings; - - var apiProxySetting = appSettings.MultifactorApiProxy; - var apiTimeoutSetting = appSettings.MultifactorApiTimeout; - - IPEndPoint serviceServerEndpoint = ParseAdapterServerEndpoint(appSettings); - - TimeSpan apiTimeout = ParseMultifactorApiTimeout(apiTimeoutSetting,out var forcedTimeout); - - if (Timeout.InfiniteTimeSpan != apiTimeout && apiTimeout < RecommendedMinimalApiTimeout) - { - if (forcedTimeout) - { - _logger.LogWarning( - "You have set the timeout to {httpRequestTimeout} seconds. The recommended minimal timeout is {recommendedApiTimeout} seconds. Lowering this threshold may cause incorrect system behavior.", - apiTimeout.TotalSeconds, - RecommendedMinimalApiTimeout.TotalSeconds); - } - else - { - _logger.LogWarning( - "You have tried to set the timeout to {httpRequestTimeout} seconds. The recommended minimal timeout is {recommendedApiTimeout} seconds. If you are sure, use the following syntax: 'value={apiTimeoutSetting}!'", - apiTimeout.TotalSeconds, - RecommendedMinimalApiTimeout.TotalSeconds, - apiTimeoutSetting); - - apiTimeout = RecommendedMinimalApiTimeout; - } - } - - var builder = new ServiceConfiguration() - .SetServiceServerEndpoint(serviceServerEndpoint) - .SetApiTimeout(apiTimeout); - - ReadMultifactorApiUrlSetting(appSettings, builder); - - if (!string.IsNullOrWhiteSpace(apiProxySetting)) - builder.SetApiProxy(apiProxySetting); - - ReadInvalidCredDelaySetting(appSettings, builder); - - var clientConfigs = _clientConfigurationsProvider.GetClientConfigurations(); - if (clientConfigs.Length == 0) - { - var generalClient = _clientConfigFactoryLdapSettings.CreateConfig(RadiusAdapterConfigurationFile.ConfigName, rootConfiguration, builder); - builder.AddClient(IPAddress.Any, generalClient).IsSingleClientMode(true); - return builder; - } - - foreach (var clientConfig in clientConfigs) - AddClient(clientConfig, builder); - - return builder; - } - - private void AddClient(RadiusAdapterConfiguration clientConfig, ServiceConfiguration builder) - { - var source = _clientConfigurationsProvider.GetSource(clientConfig); - var client = _clientConfigFactoryLdapSettings.CreateConfig(source.Name, clientConfig, builder); - - var clientSettings = clientConfig.AppSettings; - var radiusClientNasIdentifierSetting = clientSettings.RadiusClientNasIdentifier; - var radiusClientIpSetting = clientSettings.RadiusClientIp; - - if (!string.IsNullOrWhiteSpace(radiusClientNasIdentifierSetting)) - { - builder.AddClient(radiusClientNasIdentifierSetting, client); - return; - } - - if (string.IsNullOrWhiteSpace(radiusClientIpSetting)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.RadiusClientNasIdentifier, - "Either '{prop}' or '{0}' must be configured. Config name: '{1}'", - RadiusAdapterConfigurationDescription.Property(x => x.AppSettings.RadiusClientIp), - client.Name); - } - - var elements = radiusClientIpSetting.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var element in elements) - { - foreach (var ip in IPAddressRange.Parse(element)) - { - builder.AddClient(ip, client); - } - } - } - - private TimeSpan ParseMultifactorApiTimeout(string mfTimeoutSetting, out bool forcedTimeout) - { - forcedTimeout = IsForcedTimeout(mfTimeoutSetting); - if (forcedTimeout) - { - mfTimeoutSetting = mfTimeoutSetting.TrimEnd('!'); - } - - if (!TimeSpan.TryParseExact(mfTimeoutSetting, @"hh\:mm\:ss", null, System.Globalization.TimeSpanStyles.None, out var httpRequestTimeout)) - return RecommendedMinimalApiTimeout; - - if (httpRequestTimeout == TimeSpan.Zero) - return Timeout.InfiniteTimeSpan; - - return httpRequestTimeout; - } - - private bool IsForcedTimeout(string mfTimeoutSetting) => mfTimeoutSetting?.EndsWith("!") ?? false; - - private static IPEndPoint ParseAdapterServerEndpoint(AppSettingsSection appSettings) - { - if (string.IsNullOrWhiteSpace(appSettings.AdapterServerEndpoint)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.AdapterServerEndpoint, - "'{prop}' element not found. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); - } - - if (!IPEndPointFactory.TryParse(appSettings.AdapterServerEndpoint, out var serviceServerEndpoint)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.AdapterServerEndpoint, - "Can't parse '{prop}' value. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); - } - - return serviceServerEndpoint; - } - - private static void ReadInvalidCredDelaySetting(AppSettingsSection appSettings, ServiceConfiguration builder) - { - try - { - var waiterConfig = RandomWaiterConfig.Create(appSettings.InvalidCredentialDelay); - builder.SetInvalidCredentialDelay(waiterConfig); - } - catch - { - throw InvalidConfigurationException.For( - x => x.AppSettings.InvalidCredentialDelay, - "Can't parse '{prop}' value. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); - } - } - - private static void ReadMultifactorApiUrlSetting(AppSettingsSection appSettings, ServiceConfiguration builder) - { - var apiUrlSetting = appSettings.MultifactorApiUrl; - if (string.IsNullOrWhiteSpace(apiUrlSetting)) - { - throw InvalidConfigurationException.For( - x => x.AppSettings.MultifactorApiUrl, - "'{prop}' element not found. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); - } - - var urls = Utils.SplitString(apiUrlSetting); - foreach (var url in urls) - builder.AddApiUrl(url); - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/IServiceConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/IServiceConfiguration.cs deleted file mode 100644 index 1e1093c7..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/IServiceConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.ObjectModel; -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Service; - -public interface IServiceConfiguration -{ - string ApiProxy { get; } - IReadOnlyList ApiUrls { get; } - TimeSpan ApiTimeout { get; } - ReadOnlyCollection Clients { get; } - RandomWaiterConfig InvalidCredentialDelay { get; } - IPEndPoint ServiceServerEndpoint { get; } - bool SingleClientMode { get; } - IClientConfiguration? GetClient(IPAddress ip); - IClientConfiguration? GetClient(string nasIdentifier); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/ServiceConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/ServiceConfiguration.cs deleted file mode 100644 index c0fed3aa..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/ServiceConfiguration.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.ObjectModel; -using System.Configuration; -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; - -namespace Multifactor.Radius.Adapter.v2.Core.Configuration.Service; - -public class ServiceConfiguration : IServiceConfiguration -{ - private readonly List _apiUrls = new(); - /// - /// List of clients with identification by client ip - /// - private readonly IDictionary _ipClients = new Dictionary(); - - /// - /// List of clients with identification by NAS-Identifier attr - /// - private readonly IDictionary _nasIdClients = new Dictionary(); - - private readonly List _clients = new(); - public ReadOnlyCollection Clients => _clients.AsReadOnly(); - - public IClientConfiguration? GetClient(string nasIdentifier) - { - if (SingleClientMode) - return _ipClients[IPAddress.Any]; - - if (string.IsNullOrWhiteSpace(nasIdentifier)) - return null; - - if (_nasIdClients.TryGetValue(nasIdentifier, out var client)) - return client; - - return null; - } - - public IClientConfiguration? GetClient(IPAddress ip) - { - if (SingleClientMode) - return _ipClients[IPAddress.Any]; - - if (_ipClients.TryGetValue(ip, out var client)) - return client; - - return null; - } - - /// - /// This service RADIUS UDP Server endpoint - /// - public IPEndPoint ServiceServerEndpoint { get; private set; } - - /// - /// Multifactor API URLs - /// - public IReadOnlyList ApiUrls => _apiUrls; - - /// - /// HTTP Proxy for API - /// - public string ApiProxy { get; private set; } - - /// - /// HTTP timeout for Multifactor requests - /// - public TimeSpan ApiTimeout { get; private set; } - - public bool SingleClientMode { get; private set; } - public RandomWaiterConfig InvalidCredentialDelay { get; private set; } - - public ServiceConfiguration SetApiProxy(string val) - { - if (string.IsNullOrWhiteSpace(val)) - throw new ArgumentException($"'{nameof(val)}' cannot be null or whitespace.", nameof(val)); - - ApiProxy = val; - return this; - } - - public ServiceConfiguration AddApiUrl(string val) - { - if (string.IsNullOrWhiteSpace(val)) - throw new ArgumentException($"'{nameof(val)}' cannot be null or whitespace.", nameof(val)); - - if (!_apiUrls.Contains(val)) - _apiUrls.Add(val); - - return this; - } - - public ServiceConfiguration SetApiTimeout(TimeSpan httpTimeoutSetting) - { - ApiTimeout = httpTimeoutSetting; - return this; - } - - public ServiceConfiguration AddClient(string nasId, IClientConfiguration client) - { - if (_nasIdClients.TryGetValue(nasId, out var idClient)) - throw new ConfigurationErrorsException($"Client with NAS-Identifier '{nasId} already added from {idClient.Name}.config"); - - if (string.IsNullOrWhiteSpace(nasId)) - throw new ArgumentException($"'{nameof(nasId)}' cannot be null or whitespace.", nameof(nasId)); - - if (client is null) - throw new ArgumentNullException(nameof(client)); - - _nasIdClients.Add(nasId, client); - _clients.Add(client); - return this; - } - - public ServiceConfiguration AddClient(IPAddress ip, IClientConfiguration client) - { - if (ip is null) - throw new ArgumentNullException(nameof(ip)); - - if (client is null) - throw new ArgumentNullException(nameof(client)); - - if (!_ipClients.TryAdd(ip, client)) - throw new ConfigurationErrorsException($"Client with IP {ip} already added from {_ipClients[ip].Name}.config"); - - _clients.Add(client); - return this; - } - - public ServiceConfiguration SetInvalidCredentialDelay(RandomWaiterConfig config) - { - InvalidCredentialDelay = config ?? throw new ArgumentNullException(nameof(config)); - return this; - } - - public ServiceConfiguration SetServiceServerEndpoint(IPEndPoint endpoint) - { - ServiceServerEndpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); - return this; - } - - public ServiceConfiguration IsSingleClientMode(bool single) - { - SingleClientMode = single; - return this; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs deleted file mode 100644 index f6e4e302..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public class ActiveDirectoryFormatter : ILdapBindNameFormatter -{ - public LdapImplementation LdapImplementation => LdapImplementation.ActiveDirectory; - - public string FormatName(string userName, ILdapProfile ldapProfile) - { - return userName; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs deleted file mode 100644 index 135dbb63..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public class FreeIpaFormatter : ILdapBindNameFormatter -{ - public LdapImplementation LdapImplementation => LdapImplementation.FreeIPA; - - public string FormatName(string userName, ILdapProfile ldapProfile) - { - var identity = new UserIdentity(userName); - - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) - return userName; - - return ldapProfile.Dn.StringRepresentation; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs deleted file mode 100644 index a19a8f5f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public class MultiDirectoryFormatter : ILdapBindNameFormatter -{ - public LdapImplementation LdapImplementation => LdapImplementation.MultiDirectory; - - public string FormatName(string userName, ILdapProfile ldapProfile) - { - var identity = new UserIdentity(userName); - - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) - return userName; - - return ldapProfile.Dn.StringRepresentation; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs deleted file mode 100644 index f827b27b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public class OpenLdapFormatter: ILdapBindNameFormatter -{ - public LdapImplementation LdapImplementation => LdapImplementation.OpenLDAP; - - public string FormatName(string userName, ILdapProfile ldapProfile) - { - var identity = new UserIdentity(userName); - - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) - return userName; - - return ldapProfile.Dn.StringRepresentation; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs deleted file mode 100644 index 75d8ddd8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public class SambaFormatter : ILdapBindNameFormatter -{ - public LdapImplementation LdapImplementation => LdapImplementation.Samba; - - public string FormatName(string userName, ILdapProfile ldapProfile) - { - var identity = new UserIdentity(userName); - - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) - return userName; - - return ldapProfile.Dn.StringRepresentation; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessor.cs deleted file mode 100644 index 27d66578..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessor.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; - -public interface IFirstFactorProcessor -{ - // TODO remove 'context' from signature. Create ff request and response - Task ProcessFirstFactor(IRadiusPipelineExecutionContext context); - AuthenticationSource AuthenticationSource { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessorProvider.cs deleted file mode 100644 index 62724d0f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessorProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; - -public interface IFirstFactorProcessorProvider -{ - IFirstFactorProcessor GetProcessor(AuthenticationSource authSource); -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs deleted file mode 100644 index d50eff28..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; -using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; - -public class LdapFirstFactorProcessor : IFirstFactorProcessor -{ - private readonly ILdapConnectionFactory _ldapConnectionFactory; - private readonly ILdapBindNameFormatterProvider _ldapBindNameFormatterProvider; - private readonly ILogger _logger; - - public AuthenticationSource AuthenticationSource => AuthenticationSource.Ldap; - - public LdapFirstFactorProcessor(ILdapConnectionFactory ldapConnectionFactory, ILdapBindNameFormatterProvider ldapBindNameFormatterProvider, ILogger logger) - { - Throw.IfNull(ldapConnectionFactory, nameof(ldapConnectionFactory)); - Throw.IfNull(logger, nameof(logger)); - - _ldapConnectionFactory = ldapConnectionFactory; - _logger = logger; - _ldapBindNameFormatterProvider = ldapBindNameFormatterProvider; - } - - public Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) - { - ArgumentNullException.ThrowIfNull(context, nameof(context)); - - var radiusPacket = context.RequestPacket; - Throw.IfNull(radiusPacket, nameof(radiusPacket)); - - if (context.LdapServerConfiguration is null) - throw new InvalidOperationException("No Ldap servers configured."); - - if (string.IsNullOrWhiteSpace(radiusPacket.UserName)) - { - _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } - - var transformedName = UserNameTransformation.Transform(radiusPacket.UserName, context.UserNameTransformRules.BeforeFirstFactor); - - var passphrase = context.Passphrase; - if (string.IsNullOrWhiteSpace(passphrase.Raw)) - { - _logger.LogWarning("No User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } - - if (string.IsNullOrWhiteSpace(passphrase.Password)) - { - _logger.LogWarning("Can't parse User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } - - var isValid = ValidateUserCredentials(context, transformedName, passphrase.Password); - if (!isValid) - { - Reject(context); - return Task.CompletedTask; - } - - _logger.LogInformation("User '{user:l}' credential and status verified successfully at {endpoint:l}", transformedName, context.LdapServerConfiguration.ConnectionString); - Accept(context); - return Task.CompletedTask; - } - - private bool ValidateUserCredentials( - IRadiusPipelineExecutionContext context, - string login, - string password) - { - var serverConfig = context.LdapServerConfiguration; - if (serverConfig is null) - throw new InvalidOperationException("No Ldap servers configured."); - - var bindName = string.Empty; - - try - { - var ldapImpl = context.LdapSchema!.LdapServerImplementation; - var formatter = _ldapBindNameFormatterProvider.GetLdapBindNameFormatter(ldapImpl); - if (formatter is null) - _logger.LogWarning("No LDAP bind name formatter configured for '{implementation}' implementation.", ldapImpl); - - var formatted = string.Empty; - if (context.UserLdapProfile is not null) - formatted = formatter?.FormatName(login, context.UserLdapProfile); - - bindName = string.IsNullOrWhiteSpace(formatted) ? login : formatted; - - _logger.LogDebug("Use '{name}' for LDAP bind.", bindName); - using var connection = GetConnection( - serverConfig.ConnectionString, - bindName, - password, - serverConfig.BindTimeoutInSeconds); - - return true; - } - catch (Exception ex) - { - if (ex is not LdapException ldapException) - { - _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", bindName, serverConfig.ConnectionString); - return false; - } - - var info = GetLdapErrorInfo(ldapException); - if (info != null) - ProcessErrorReason(info, context, serverConfig); - - _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", bindName, serverConfig.ConnectionString, info?.ReasonText); - } - - return false; - } - - private ILdapConnection GetConnection(string connectionString, string userName, string password, int bindTimeoutInSeconds) - { - var connectionOptions = new LdapConnectionOptions( - new LdapConnectionString(connectionString), - AuthType.Basic, - userName, - password, - TimeSpan.FromSeconds(bindTimeoutInSeconds)); - - return _ldapConnectionFactory.CreateConnection(connectionOptions); - } - - private void Reject(IRadiusPipelineExecutionContext context) - { - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - } - - private void Accept(IRadiusPipelineExecutionContext context) - { - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; - } - - private LdapErrorReasonInfo? GetLdapErrorInfo(LdapException exception) - { - if (string.IsNullOrWhiteSpace(exception.ServerErrorMessage)) - return null; - var reason = LdapErrorReasonInfo.Create(exception.ServerErrorMessage); - return reason; - } - - private void ProcessErrorReason(LdapErrorReasonInfo errorInfo, IRadiusPipelineExecutionContext context, ILdapServerConfiguration ldapServerConfiguration) - { - if (errorInfo.Flags.HasFlag(LdapErrorFlag.MustChangePassword)) - context.MustChangePasswordDomain = ldapServerConfiguration.ConnectionString; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/ForestFilter.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/ForestFilter.cs deleted file mode 100644 index a62242fa..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/ForestFilter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; - -public class ForestFilter : IForestFilter -{ - public IEnumerable FilterDomains(IEnumerable domains, IPermissionRules permission) - { - ArgumentNullException.ThrowIfNull(domains); - ArgumentNullException.ThrowIfNull(permission); - - return domains.Where(x => permission.IsPermitted(LdapNamesUtils.DnToFqdn(x.Schema.NamingContext))); - } - - public IEnumerable FilterSuffixes(IEnumerable domains, IPermissionRules permission) - { - var result = new List(); - foreach (var domain in domains) - { - var allowedSuffixes = domain.Suffixes.Where(permission.IsPermitted); - result.Add(new LdapForestEntry(domain.Schema, allowedSuffixes)); - } - - return result; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/IForestFilter.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/IForestFilter.cs deleted file mode 100644 index 8bee8184..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/IForestFilter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; - -public interface IForestFilter -{ - IEnumerable FilterDomains(IEnumerable domains, IPermissionRules permission); - - IEnumerable FilterSuffixes(IEnumerable domains, IPermissionRules permission); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/LdapForestEntry.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/LdapForestEntry.cs deleted file mode 100644 index c5d3d510..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/LdapForestEntry.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; - -public class LdapForestEntry -{ - private readonly HashSet _suffixes = new(); - public ILdapSchema Schema { get; } - public IReadOnlyCollection Suffixes => _suffixes; - - public LdapForestEntry(ILdapSchema schema) - { - ArgumentNullException.ThrowIfNull(schema); - Schema = schema; - } - - public LdapForestEntry(ILdapSchema schema, IEnumerable suffixes) - { - ArgumentNullException.ThrowIfNull(schema); - ArgumentNullException.ThrowIfNull(suffixes); - - Schema = schema; - foreach (var suffix in suffixes) - Add(suffix); - } - - public void AddSuffix(string suffix) - { - ArgumentException.ThrowIfNullOrWhiteSpace(suffix); - Add(suffix); - } - - public void AddSuffix(IEnumerable suffix) - { - ArgumentNullException.ThrowIfNull(suffix); - foreach (var s in suffix) - Add(s); - } - - private void Add(string suffix) - { - _suffixes.Add(NormalizeSuffix(suffix)); - } - - private string NormalizeSuffix(string suffix) => suffix.ToLower().Trim(); - -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnection.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnection.cs deleted file mode 100644 index 72336a15..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.ObjectModel; -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Core.Ldap.Entry; -using Multifactor.Core.Ldap.Name; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; - -public interface ILdapConnection : Multifactor.Core.Ldap.Connection.ILdapConnection -{ - ReadOnlyCollection Find( - DistinguishedName searchBase, - string filter, - SearchScope scope, - PageResultRequestControl? pageControl = null, - params LdapAttributeName[] attributes); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnectionFactory.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnectionFactory.cs deleted file mode 100644 index dcc68388..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnectionFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Core.Ldap.Connection; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; - -public interface ILdapConnectionFactory -{ - ILdapConnection CreateConnection(LdapConnectionOptions ldapConnectionOptions); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentity.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentity.cs deleted file mode 100644 index cbe326cc..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentity.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Multifactor.Core.Ldap.LangFeatures; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -public class UserIdentity -{ - public string Identity { get; private set; } - public UserIdentityFormat Format { get; private set; } - - public UserIdentity(string identity) - { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); - Identity = identity; - Format = GetIdentityTypeByIdentity(identity); - } - - public UserIdentity(string identity, UserIdentityFormat format) - { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); - Identity = identity; - Format = format; - } - - public UserIdentityFormat GetIdentityTypeByIdentity(string identity) - { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); - - var id = identity.ToLower(); - - if (id.Contains("\\")) - return UserIdentityFormat.NetBiosName; - - if (id.Contains('=')) - return UserIdentityFormat.DistinguishedName; - - if (id.Contains('@')) - return UserIdentityFormat.UserPrincipalName; - - return UserIdentityFormat.SamAccountName; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnectionStringExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnectionStringExtensions.cs deleted file mode 100644 index 3d1ac8b6..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnectionStringExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Multifactor.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; - -public static class LdapConnectionStringExtensions -{ - /// - /// Copy LDAP schema and port from ldapConnectionString with new host - /// Required to create the same connection to a new host. - /// - public static LdapConnectionString CopySchemaAndPort(this LdapConnectionString ldapConnectionString, string newHost) - { - var initialLdapSchema = ldapConnectionString.Scheme; - var initialLdapPort = ldapConnectionString.Port; - return new LdapConnectionString($"{initialLdapSchema}://{newHost}:{initialLdapPort}"); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapErrorReasonInfo.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapErrorReasonInfo.cs deleted file mode 100644 index f3886e7d..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapErrorReasonInfo.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.ComponentModel; -using System.Text.RegularExpressions; -using Multifactor.Core.Ldap.LangFeatures; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; - -public class LdapErrorReasonInfo -{ - public LdapErrorFlag Flags { get; } - public LdapErrorReason Reason { get; } - public string ReasonText { get; } - - protected LdapErrorReasonInfo(LdapErrorReason reason, LdapErrorFlag flags, string reasonText) - { - Flags = flags; - Reason = reason; - ReasonText = reasonText; - } - - public static LdapErrorReasonInfo Create(string serverErrorMessage) - { - Throw.IfNullOrWhiteSpace(serverErrorMessage, nameof(serverErrorMessage)); - - var reason = GetErrorReason(serverErrorMessage); - var flags = GetErrorFlags(reason); - var text = GetReasonText(reason); - - return new LdapErrorReasonInfo(reason, flags, text); - } - - private static LdapErrorReason GetErrorReason(string message) - { - if (string.IsNullOrWhiteSpace(message)) - { - return LdapErrorReason.UnknownError; - } - - var pattern = @"data ([0-9a-e]{3})"; - var match = Regex.Match(message, pattern); - - if (!match.Success || match.Groups.Count != 2) - { - return LdapErrorReason.UnknownError; - } - - var data = match.Groups[1].Value; - switch (data) - { - case "525": return LdapErrorReason.UserNotFound; - case "52e": return LdapErrorReason.InvalidCredentials; - case "530": return LdapErrorReason.NotPermittedToLogonAtThisTime; - case "531": return LdapErrorReason.NotPermittedToLogonAtThisWorkstation; - case "532": return LdapErrorReason.PasswordExpired; - case "533": return LdapErrorReason.AccountDisabled; - case "701": return LdapErrorReason.AccountExpired; - case "773": return LdapErrorReason.UserMustChangePassword; - case "775": return LdapErrorReason.UserAccountLocked; - default: return LdapErrorReason.UnknownError; - } - } - - private static LdapErrorFlag GetErrorFlags(LdapErrorReason reason) - { - switch (reason) - { - case LdapErrorReason.PasswordExpired: - case LdapErrorReason.UserMustChangePassword: - return LdapErrorFlag.MustChangePassword; - default: - return LdapErrorFlag.None; - } - } - - private static string GetReasonText(LdapErrorReason reason) - { - // "SomeErrorText" -> ["some, "error", "text"] - var splitted = Regex.Split(reason.ToString(), @"(? x.ToLower()); - return string.Join(" ", splitted); - } -} - -public enum LdapErrorReason -{ - [Description("525")] - UserNotFound, - - [Description("52e")] - InvalidCredentials, - - [Description("530")] - NotPermittedToLogonAtThisTime, - - [Description("531")] - NotPermittedToLogonAtThisWorkstation, - - [Description("532")] - PasswordExpired, - - [Description("533")] - AccountDisabled, - - [Description("701")] - AccountExpired, - - [Description("773")] - UserMustChangePassword, - - [Description("775")] - UserAccountLocked, - - UnknownError -} - -public enum LdapErrorFlag -{ - None = 0, - MustChangePassword = 1, -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapNamesUtils.cs b/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapNamesUtils.cs deleted file mode 100644 index ff44831a..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapNamesUtils.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Multifactor.Core.Ldap.Name; - -namespace Multifactor.Radius.Adapter.v2.Core.Ldap; - -public static class LdapNamesUtils -{ - /// - /// Converts domain.local to DC=domain,DC=local - /// - public static DistinguishedName FqdnToDn(string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - - var portIndex = name.IndexOf(':'); - if (portIndex > 0) - { - name = name[..portIndex]; - } - - var domains = name.Split(['.'], StringSplitOptions.RemoveEmptyEntries); - var dnParts = domains.Select(p => $"DC={p}").ToArray(); - var dn = string.Join(",", dnParts); - return new DistinguishedName(dn); - } - - public static string DnToFqdn(DistinguishedName name) - { - var ncs = name.Components.Reverse(); - return string.Join(".", ncs.Select(x => x.Value)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ApiCredential.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ApiCredential.cs deleted file mode 100644 index 98ffb52c..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ApiCredential.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi -{ - public record ApiCredential - { - public string Usr { get; } - public string Pwd { get; } - - public ApiCredential(string key, string secret) - { - if (string.IsNullOrWhiteSpace(key)) - { - throw new ArgumentException($"'{nameof(key)}' cannot be null or whitespace.", nameof(key)); - } - - if (string.IsNullOrWhiteSpace(secret)) - { - throw new ArgumentException($"'{nameof(secret)}' cannot be null or whitespace.", nameof(secret)); - } - - Usr = key; - Pwd = secret; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/Capabilities.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/Capabilities.cs deleted file mode 100644 index 748252c0..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/Capabilities.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public class Capabilities -{ - public bool InlineEnroll { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ChallengeRequest.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ChallengeRequest.cs deleted file mode 100644 index 12f1a1a0..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ChallengeRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public class ChallengeRequest -{ - public string Identity { get; set; } = string.Empty; - public string Challenge { get; set; } = string.Empty; - public string RequestId { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/GroupPolicyPreset.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/GroupPolicyPreset.cs deleted file mode 100644 index 5c69eae1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/GroupPolicyPreset.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public class GroupPolicyPreset -{ - public string SignUpGroups { get; set; } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorApiResponse.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorApiResponse.cs deleted file mode 100644 index 2c344c68..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorApiResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public class MultiFactorApiResponse -{ - public bool Success { get; set; } - public TModel Model { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorResponse.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorResponse.cs deleted file mode 100644 index 90c73824..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth; - -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public class MultifactorResponse -{ - public AuthenticationStatus Code { get; } - - public string? ReplyMessage { get; } - public string? State { get; } = null; - - public MultifactorResponse(AuthenticationStatus code, string? state = null, string? replyMessage = null) - { - Code = code; - ReplyMessage = replyMessage; - State = state; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyModeDescriptor.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyModeDescriptor.cs deleted file mode 100644 index 0375ca11..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyModeDescriptor.cs +++ /dev/null @@ -1,72 +0,0 @@ -//Copyright(c) 2022 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; - -public class PrivacyModeDescriptor -{ - private readonly string[] _fields; - public PrivacyMode Mode { get; } - - public static PrivacyModeDescriptor Default => new(PrivacyMode.None); - - public bool HasField(string field) - { - if (string.IsNullOrWhiteSpace(field)) - { - return false; - } - - return _fields.Any(x => x.Equals(field, StringComparison.OrdinalIgnoreCase)); - } - - private PrivacyModeDescriptor(PrivacyMode mode, params string[] fields) - { - Mode = mode; - _fields = fields ?? throw new ArgumentNullException(nameof(fields)); - } - - public static PrivacyModeDescriptor Create(string? value) - { - if (string.IsNullOrWhiteSpace(value)) return new PrivacyModeDescriptor(PrivacyMode.None); - - var mode = GetMode(value); - if (mode != PrivacyMode.Partial) return new PrivacyModeDescriptor(mode); - - var fields = GetFields(value); - return new PrivacyModeDescriptor(mode, fields); - } - - private static PrivacyMode GetMode(string value) - { - if (int.TryParse(value, out _)) - throw new Exception("Unexpected privacy-mode value"); - - var index = value.IndexOf(':'); - if (index == -1) - { - if (!Enum.TryParse(value, true, out PrivacyMode parsed1)) - throw new Exception("Unexpected privacy-mode value"); - return parsed1; - } - - var sub = value[..index]; - if (!Enum.TryParse(sub, true, out var parsed2)) - throw new Exception("Unexpected privacy-mode value"); - - return parsed2; - } - - private static string[] GetFields(string value) - { - var index = value.IndexOf(':'); - if (index == -1 || value.Length <= index + 1) - { - return Array.Empty(); - } - - var sub = value[(index + 1)..]; - return sub.Split(',', StringSplitOptions.RemoveEmptyEntries).Distinct().ToArray(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs b/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs deleted file mode 100644 index 5a5adace..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -public enum RequestStatus -{ - AwaitingAuthentication, - Granted, - Denied -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ExecutionState.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ExecutionState.cs deleted file mode 100644 index cfa053cc..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ExecutionState.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; - -public class ExecutionState : IExecutionState -{ - private bool _isTerminated; - private bool _shouldSkip; - - public bool IsTerminated => _isTerminated; - public bool ShouldSkipResponse => _shouldSkip; - - public void Terminate() - { - _isTerminated = true; - } - - public void SkipResponse() - { - _shouldSkip = true; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IExecutionState.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IExecutionState.cs deleted file mode 100644 index f413565f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IExecutionState.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; - -public interface IExecutionState -{ - public bool IsTerminated { get; } - public bool ShouldSkipResponse { get; } - - public void Terminate(); - - public void SkipResponse(); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs deleted file mode 100644 index 28b511d8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; - -public interface IResponseInformation -{ - string? ReplyMessage { get; set; } - - public string? State { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseSender.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseSender.cs deleted file mode 100644 index 40af89b8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseSender.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; - -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; - -public interface IResponseSender -{ - Task SendResponse(SendAdapterResponseRequest context); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs deleted file mode 100644 index 629a9927..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; - -public class ResponseInformation : IResponseInformation -{ - public string? ReplyMessage { get; set; } - - public string? State { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/IPipelineExecutionSettings.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/IPipelineExecutionSettings.cs deleted file mode 100644 index a743c04e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/IPipelineExecutionSettings.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline.Settings; - -public interface IPipelineExecutionSettings -{ - ILdapServerConfiguration? LdapServerConfiguration { get; } - AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; } - bool BypassSecondFactorWhenApiUnreachable { get; } - AuthenticationSource FirstFactorAuthenticationSource { get; } - ApiCredential ApiCredential { get; } - IReadOnlySet NpsServerEndpoints { get; } - TimeSpan NpsServerTimeout { get; } - PrivacyModeDescriptor PrivacyMode { get; } - IReadOnlyDictionary RadiusReplyAttributes { get; } - IPEndPoint ServiceClientEndpoint { get; } - string SignUpGroups { get; } - UserNameTransformRules UserNameTransformRules { get; } - RandomWaiterConfig InvalidCredentialDelay { get; } - PreAuthModeDescriptor PreAuthnMode { get; } - string ClientConfigurationName { get; } - SharedSecret RadiusSharedSecret { get; } - IReadOnlyList IpWhiteList { get; } - IReadOnlyList ApiUrls { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/PipelineExecutionSettings.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/PipelineExecutionSettings.cs deleted file mode 100644 index b27a3c96..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/PipelineExecutionSettings.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Net; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline.Settings; - -public class PipelineExecutionSettings : IPipelineExecutionSettings -{ - private readonly IClientConfiguration _configuration; - private readonly SharedSecret _sharedSecret; - public ILdapServerConfiguration? LdapServerConfiguration { get; } - public AuthenticatedClientCacheConfig AuthenticationCacheLifetime => _configuration.AuthenticationCacheLifetime; - public bool BypassSecondFactorWhenApiUnreachable => _configuration.BypassSecondFactorWhenApiUnreachable; - public AuthenticationSource FirstFactorAuthenticationSource => _configuration.FirstFactorAuthenticationSource; - public ApiCredential ApiCredential => _configuration.ApiCredential; - public IReadOnlySet NpsServerEndpoints => _configuration.NpsServerEndpoints; - public TimeSpan NpsServerTimeout => _configuration.NpsServerTimeout; - public PrivacyModeDescriptor PrivacyMode => _configuration.PrivacyMode; - public IReadOnlyDictionary RadiusReplyAttributes => _configuration.RadiusReplyAttributes; - public IPEndPoint ServiceClientEndpoint => _configuration.ServiceClientEndpoint; - public string SignUpGroups => _configuration.SignUpGroups; - public UserNameTransformRules UserNameTransformRules => _configuration.UserNameTransformRules; - public RandomWaiterConfig InvalidCredentialDelay => _configuration.InvalidCredentialDelay; - public PreAuthModeDescriptor PreAuthnMode => _configuration.PreAuthnMode; - public SharedSecret RadiusSharedSecret => _sharedSecret; - public IReadOnlyList ApiUrls => _configuration.ApiUrls; - public IReadOnlyList IpWhiteList => _configuration.IpWhiteList; - public string ClientConfigurationName => _configuration.Name; - - public PipelineExecutionSettings(IClientConfiguration clientConfiguration, ILdapServerConfiguration? ldapServerConfiguration = null) - { - Throw.IfNull(clientConfiguration, nameof(clientConfiguration)); - - _configuration = clientConfiguration; - _sharedSecret = new SharedSecret(clientConfiguration.RadiusSharedSecret); - LdapServerConfiguration = ldapServerConfiguration; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/RadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/RadiusDictionary.cs deleted file mode 100644 index 0508f523..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/RadiusDictionary.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes -{ - public class RadiusDictionary : IRadiusDictionary - { - private readonly Dictionary _attributes = new(); - private readonly List _vendorSpecificAttributes = new(); - private readonly Dictionary _attributeNames = new(); - private readonly ApplicationVariables _variables; - private readonly string? _filePath; - - /// - /// Load the dictionary from a dictionary file - /// - public RadiusDictionary(ApplicationVariables variables, string? filePath = null) - { - _variables = variables; - _filePath = filePath; - } - - public void Read() - { - var stringBuilder = new StringBuilder(_variables.AppPath); - stringBuilder.Append(_filePath ?? $"{Path.DirectorySeparatorChar}content{Path.DirectorySeparatorChar}radius.dictionary"); - - var path = stringBuilder.ToString(); - using var sr = new StreamReader(path); - - while (sr.Peek() != -1) - { - var line = sr.ReadLine(); - - if (line.StartsWith("Attribute")) - { - var lineparts = line.Split(new char[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries); - var key = Convert.ToByte(lineparts[1]); - - // If duplicates are encountered, the last one will prevail - if (_attributes.ContainsKey(key)) - { - _attributes.Remove(key); - } - - if (_attributeNames.ContainsKey(lineparts[2])) - { - _attributeNames.Remove(lineparts[2]); - } - - var attributeDefinition = new DictionaryAttribute(lineparts[2], key, lineparts[3]); - _attributes.Add(key, attributeDefinition); - _attributeNames.Add(attributeDefinition.Name, attributeDefinition); - - continue; - } - - if (line.StartsWith("VendorSpecificAttribute")) - { - var lineparts = line.Split(new char[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries); - var vsa = new DictionaryVendorAttribute( - Convert.ToUInt32(lineparts[1]), - lineparts[3], - Convert.ToUInt32(lineparts[2]), - lineparts[4]); - - _vendorSpecificAttributes.Add(vsa); - - if (_attributeNames.ContainsKey(vsa.Name)) - { - _attributeNames.Remove(vsa.Name); - } - - _attributeNames.Add(vsa.Name, vsa); - - continue; - } - } - } - - public string GetInfo() - { - return $"Parsed {_attributes.Count} attributes and {_vendorSpecificAttributes.Count} vendor attributes from the radius.dictionary file"; - } - - public DictionaryVendorAttribute? GetVendorAttribute(uint vendorId, byte vendorCode) - { - return _vendorSpecificAttributes.FirstOrDefault(o => o.VendorId == vendorId && o.VendorCode == vendorCode); - } - - public DictionaryAttribute GetAttribute(byte typecode) - { - return _attributes[typecode]; - } - - public DictionaryAttribute GetAttribute(string name) - { - _attributeNames.TryGetValue(name, out var attributeType); - return attributeType; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusAttributeCode.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusAttributeCode.cs deleted file mode 100644 index ee5c7f1f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusAttributeCode.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Metadata -{ - internal static class RadiusAttributeCode - { - /// - /// User-Password - /// - public const int UserPassword = 2; - - /// - /// Vendor-Specific - /// - public const int VendorSpecific = 26; - - /// - /// Message-Authenticator - /// - public const int MessageAuthenticator = 80; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusFieldOffsets.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusFieldOffsets.cs deleted file mode 100644 index 5def7adc..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusFieldOffsets.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Metadata -{ - internal static class RadiusFieldOffsets - { - public const int CodeFieldPosition = 0; - public const int IdentifierFieldPosition = 1; - - public const int LengthFieldPosition = 2; - public const int LengthFieldLength = 2; - - public const int AuthenticatorFieldPosition = 4; - public const int AuthenticatorFieldLength = 16; - - public const int AttributesFieldPosition = 20; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/IRadiusPacket.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/IRadiusPacket.cs deleted file mode 100644 index 0678a92a..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/IRadiusPacket.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Net; - -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Packet; - -public interface IRadiusPacket -{ - PacketCode Code { get; } - byte Identifier { get; } - RadiusAuthenticator Authenticator { get; } - RadiusAuthenticator? RequestAuthenticator { get; } - AuthenticationType AuthenticationType { get; } - string? UserName { get; } - bool IsEapMessageChallenge { get; } - bool IsVendorAclRequest { get; } - bool IsWinLogon { get; } - bool IsOpenVpnStaticChallenge { get; } - string? MsClientMachineAccountNameAttribute { get; } - string? MsRasClientNameAttribute { get; } - string? CallingStationIdAttribute { get; } - string? RemoteHostName { get; } - string? CalledStationIdAttribute { get; } - string? NasIdentifierAttribute { get; } - string? State { get; } - public IPEndPoint? ProxyEndpoint { get; set; } - public IPEndPoint RemoteEndpoint { get; set; } - string? TryGetUserPassword(); - string? TryGetChallenge(); - IReadOnlyDictionary Attributes { get; } - T GetAttribute(string name); - List GetAttributes(string name); - string? GetAttributeValueAsString(string name); - string CreateUniqueKey(IPEndPoint remoteEndpoint); - void ReplaceAttribute(string name, params object[] values); - void AddAttributeValue(string name, object? value); - AccountType AccountType { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Radius/SharedSecret.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/SharedSecret.cs deleted file mode 100644 index 44eec2fc..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/SharedSecret.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.Core.Radius -{ - /// - /// Used to encrypt and decrypt user password - /// - public class SharedSecret - { - public byte[] Bytes { get; } - - public SharedSecret(string secret) - { - if (string.IsNullOrWhiteSpace(secret)) - { - throw new ArgumentException($"'{nameof(secret)}' cannot be null or whitespace.", nameof(secret)); - } - - Bytes = Encoding.UTF8.GetBytes(secret); - } - - public SharedSecret(byte[] secret) - { - if (secret is null) - { - throw new ArgumentNullException(nameof(secret)); - } - - if (secret.Length == 0) - { - throw new ArgumentException("Empty secret", nameof(secret)); - } - - Bytes = secret; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/RandomWaiterFeature/RandomWaiterConfig.cs b/src/Multifactor.Radius.Adapter.v2/Core/RandomWaiterFeature/RandomWaiterConfig.cs deleted file mode 100644 index 895703ce..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/RandomWaiterFeature/RandomWaiterConfig.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature -{ - public class RandomWaiterConfig - { - public int Min { get; } - public int Max { get; } - public bool ZeroDelay { get; } - - protected RandomWaiterConfig(int min, int max) - { - Min = min; - Max = max; - ZeroDelay = min == 0 && min == max; - } - - public static RandomWaiterConfig Create(string delaySettings) - { - if (string.IsNullOrWhiteSpace(delaySettings)) - { - return new RandomWaiterConfig(0, 0); - } - - if (int.TryParse(delaySettings, out var delay)) - { - if (delay < 0) Throw(); - return new RandomWaiterConfig(delay, delay); - } - - var splitted = delaySettings.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); - if (splitted.Length != 2) Throw(); - - var values = splitted.Select(x => int.TryParse(x, out var d) ? d : -1).ToArray(); - if (values.Any(x => x < 0)) Throw(); - - return new RandomWaiterConfig(values[0], values[1]); - } - - private static void Throw() - { - throw new ArgumentException("Incorrect delay configuration"); - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Core/UserNameTransformation.cs b/src/Multifactor.Radius.Adapter.v2/Core/UserNameTransformation.cs deleted file mode 100644 index f728f26b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/UserNameTransformation.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.RegularExpressions; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -namespace Multifactor.Radius.Adapter.v2.Core; - -public class UserNameTransformation -{ - internal static string Transform(string userName, UserNameTransformRule[] rules) - { - Throw.IfNullOrWhiteSpace(userName, nameof(userName)); - Throw.IfNull(rules, nameof(rules)); - - foreach (var rule in rules) - { - if (string.IsNullOrWhiteSpace(rule.Match)) - continue; - - var regex = new Regex(rule.Match); - if (rule.Count > 0) - { - userName = regex.Replace(userName, rule.Replace, rule.Count); - } - else - { - userName = regex.Replace(userName, rule.Replace); - } - } - - return userName; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Utils.cs b/src/Multifactor.Radius.Adapter.v2/Core/Utils.cs deleted file mode 100644 index 7f6f7c80..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Utils.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.DirectoryServices.Protocols; -using System.Text; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Core -{ - public static class Utils - { - /// - /// Convert a string of hex encoded bytes to a byte array - /// - public static byte[] StringToByteArray(string hex) - { - var NumberChars = hex.Length; - var bytes = new byte[NumberChars / 2]; - for (var i = 0; i < NumberChars; i += 2) - { - bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - } - - return bytes; - } - - - /// - /// Convert a byte array to a string of hex encoded bytes - /// - public static string ToHexString(this byte[] bytes) - { - return bytes != null ? BitConverter.ToString(bytes).ToLowerInvariant().Replace("-", "") : null; - } - - /// - /// Base64 encoded string - /// - public static string Base64(this byte[] bytes) - { - if (bytes != null) - return Convert.ToBase64String(bytes); - - return null; - } - - /// - /// Converts string from base64 to utf-8 - /// - /// - /// - public static string Base64toUtf8(this string st) - { - return Encoding.UTF8.GetString(Convert.FromBase64String(st)); - } - - /// - /// User name without domain - /// - public static string CanonicalizeUserName(string? userName) - { - if (string.IsNullOrWhiteSpace(userName)) - return string.Empty; - - var identity = userName.ToLower(); - - var index = identity.IndexOf('\\', StringComparison.Ordinal); - if (index > 0) - identity = identity[(index + 1)..]; - - index = identity.IndexOf('@', StringComparison.Ordinal); - if (index > 0) - identity = identity[..index]; - - return identity; - } - - /// - /// Check if username does not contains domain prefix or suffix - /// - public static bool IsCanicalUserName(string? userName) - { - if (string.IsNullOrWhiteSpace(userName)) - return true; - - return userName.IndexOfAny(['\\', '@']) == -1; - } - - public static string[] SplitString(string? target, string separator = ";") => target - ?.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; - - public static LdapConnectionOptions CreateLdapConnectionOptions(ILdapServerConfiguration serverConfiguration) => - new(new LdapConnectionString(serverConfiguration.ConnectionString), - AuthType.Basic, - serverConfiguration.UserName, - serverConfiguration.Password, - TimeSpan.FromSeconds(serverConfiguration.BindTimeoutInSeconds)); - - public static string GetUpnSuffix(UserIdentity userIdentity) - { - if (userIdentity.Format != UserIdentityFormat.UserPrincipalName) - return string.Empty; - - var suffix = userIdentity.Identity.Split('@', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Last(); - return suffix; - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Dockerfile b/src/Multifactor.Radius.Adapter.v2/Dockerfile index b1b93e2c..60f7c357 100644 --- a/src/Multifactor.Radius.Adapter.v2/Dockerfile +++ b/src/Multifactor.Radius.Adapter.v2/Dockerfile @@ -1,21 +1,46 @@ -FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base -USER $APP_UID +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["RediusV2/RediusV2.csproj", "RediusV2/"] -RUN dotnet restore "RediusV2/RediusV2.csproj" +COPY ["RadiusV2/RadiusV2.csproj", "RadiusV2/"] +RUN dotnet restore "RadiusV2/RadiusV2.csproj" COPY . . -WORKDIR "/src/RediusV2" -RUN dotnet build "RediusV2.csproj" -c $BUILD_CONFIGURATION -o /app/build +WORKDIR "/src/RadiusV2" +RUN dotnet build "RadiusV2.csproj" -c $BUILD_CONFIGURATION -o /app/build FROM build AS publish ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "RediusV2.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +RUN dotnet publish "RadiusV2.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false -FROM base AS final +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +USER root + +# Устанавливаем библиотеки LDAP +RUN apt-get update && \ + apt-get install -y \ + libldap-2.4-2 \ + libldap-common \ + libsasl2-2 \ + libsasl2-modules && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Создаем симлинк в директории .NET +RUN DOTNET_DIR=$(dirname $(find /usr/share/dotnet -name "System.Private.CoreLib.dll" | head -1)) && \ + ln -sf /usr/lib/x86_64-linux-gnu/libldap-2.4.so.2 $DOTNET_DIR/libldap-2.4.so.2 && \ + ln -sf /usr/lib/x86_64-linux-gnu/liblber-2.4.so.2 $DOTNET_DIR/liblber-2.4.so.2 + +USER app + +# Копируем опубликованное приложение COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "RediusV2.dll"] +ENTRYPOINT ["dotnet", "RadiusV2.dll"] \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Exceptions/LdapUserNotFoundException.cs b/src/Multifactor.Radius.Adapter.v2/Exceptions/LdapUserNotFoundException.cs deleted file mode 100644 index 17e14e67..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Exceptions/LdapUserNotFoundException.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Exceptions -{ - internal class LdapUserNotFoundException : Exception - { - public LdapUserNotFoundException(string user, string domain) - : base($"User '{user}' not found at domain '{domain}'") { } - - public LdapUserNotFoundException(string user, string domain, Exception inner) - : base($"User '{user}' not found at domain '{domain}': {inner.Message}", inner) { } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Exceptions/MultifactorApiUnreachableException.cs b/src/Multifactor.Radius.Adapter.v2/Exceptions/MultifactorApiUnreachableException.cs deleted file mode 100644 index 971068a3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Exceptions/MultifactorApiUnreachableException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Exceptions; - -[Serializable] -public class MultifactorApiUnreachableException : Exception -{ - public MultifactorApiUnreachableException() { } - public MultifactorApiUnreachableException(string message) : base(message) { } - public MultifactorApiUnreachableException(string message, Exception inner) : base(message, inner) { } - protected MultifactorApiUnreachableException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 8b2a8ee8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Runtime.InteropServices; -using System.Security.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Http.Resilience; -using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; -using Multifactor.Core.Ldap.LdapGroup.Load; -using Multifactor.Core.Ldap.LdapGroup.Membership; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client.Build; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; -using Multifactor.Radius.Adapter.v2.Infrastructure.Http; -using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Server; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.DataProtection; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; -using Multifactor.Radius.Adapter.v2.Services.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Services.Radius; -using Polly; -using Serilog; -using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; - -namespace Multifactor.Radius.Adapter.v2.Extensions; - -public static class ServiceCollectionExtensions -{ - public static void AddPipeline( - this IServiceCollection services, - string pipelineKey, - PipelineConfiguration pipelineConfiguration) - { - if (pipelineConfiguration is null) - throw new ArgumentNullException(nameof(pipelineConfiguration)); - - foreach (var stepType in pipelineConfiguration.PipelineStepsTypes) - { - if (!typeof(IRadiusPipelineStep).IsAssignableFrom(stepType)) - { - throw new ArgumentException( - $"The type {stepType.FullName} does not implement {nameof(IRadiusPipelineStep)}"); - } - - services.TryAddTransient(stepType); - } - - services.TryAddTransient(); - services.AddKeyedSingleton(pipelineKey, (serviceProvider, x) => - { - var pipelineBuilder = serviceProvider.GetRequiredService(); - foreach (var type in pipelineConfiguration.PipelineStepsTypes) - { - var step = (IRadiusPipelineStep)serviceProvider.GetRequiredService(type); - pipelineBuilder.AddPipelineStep(step); - } - - return pipelineBuilder.Build()!; - }); - } - - public static void AddPipelines(this IServiceCollection services) - { - services.AddPipelineSteps(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - } - - public static void AddConfiguration(this IServiceCollection services) - { - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(prov => - { - var rootConfig = RadiusAdapterConfigurationProvider.GetRootConfiguration(); - var factory = prov.GetRequiredService(); - - var config = factory.CreateConfig(rootConfig); - - return config; - }); - } - - public static void AddUdpClient(this IServiceCollection services) - { - services.AddSingleton(prov => - { - var config = prov.GetService(); - if (config == null) - throw new NullReferenceException("Provided service configuration is null"); - return new CustomUdpClient(config.ServiceServerEndpoint); - }); - } - - public static void AddMultifactorHttpClient(this IServiceCollection services) - { - services.AddSingleton(); - services.AddHttpClient(nameof(MultifactorHttpClient), (prov, client) => - { - var config = prov.GetRequiredService(); - client.Timeout = config.ApiTimeout; - }).ConfigurePrimaryHttpMessageHandler(prov => - { - var config = prov.GetRequiredService(); - var handler = new HttpClientHandler - { - MaxConnectionsPerServer = 100, - SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 - }; - - if (string.IsNullOrWhiteSpace(config.ApiProxy)) - return handler; - - if (!WebProxyFactory.TryCreateWebProxy(config.ApiProxy, out var webProxy)) - throw new Exception( - "Unable to initialize WebProxy. Please, check whether multifactor-api-proxy URI is valid."); - - handler.Proxy = webProxy; - - return handler; - }) - .AddResilienceHandler("mf-api-pipeline", x => - { - x.AddRetry(new HttpRetryStrategyOptions - { - MaxRetryAttempts = 2, - Delay = TimeSpan.FromSeconds(1), - BackoffType = DelayBackoffType.Exponential - }); - }); - } - - public static void AddRadiusDictionary(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(prov => - { - var dict = prov.GetRequiredService(); - dict.Read(); - return dict; - }); - } - - public static void AddFirstFactor(this IServiceCollection services) - { - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } - - private static void AddPipelineSteps(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } - - public static void AddLdapSchemaLoader(this IServiceCollection services) - { - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(); - } - - public static void AddDataProtectionService(this IServiceCollection services) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - services.AddTransient(); - else - services.AddTransient(); - } - - public static void AddChallenge(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - } - - public static void AddServices(this IServiceCollection services) - { - services.AddTransient(); - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(LdapConnectionFactory.Create()); - services.AddSingleton((prov) => new CustomLdapConnectionFactory()); - - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - - services.AddTransient(); - - services.AddTransient(); - services.AddSingleton(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - AddTrustedDomains(services); - services.AddSingleton(); - AddLdapBindNameFormation(services); - } - - public static void AddAdapterLogging(this IServiceCollection services) - { - var rootConfig = RadiusAdapterConfigurationProvider.GetRootConfiguration(); - var logger = SerilogLoggerFactory.CreateLogger(rootConfig); - Log.Logger = logger; - - services.AddSerilog(); - } - - private static void AddLdapBindNameFormation(IServiceCollection services) - { - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } - - private static void AddTrustedDomains(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddTransient(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Extensions/ToPascalCaseExtension.cs b/src/Multifactor.Radius.Adapter.v2/Extensions/ToPascalCaseExtension.cs deleted file mode 100644 index adef2e7f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Extensions/ToPascalCaseExtension.cs +++ /dev/null @@ -1,22 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Extensions; - -internal static class ToPascalCaseExtension -{ - public static string ToPascalCase(this string dashCase) - { - if (string.IsNullOrWhiteSpace(dashCase)) - { - throw new ArgumentException($"'{nameof(dashCase)}' cannot be null or whitespace.", nameof(dashCase)); - } - - var splitted = dashCase.Split(new[] { '-' }); - var upperFirstChar = splitted.Select(x => $"{char.ToUpperInvariant(x[0])}{x[1..]}"); - var pascalCase = string.Join(string.Empty, upperFirstChar); - - return pascalCase; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationBuilderExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationBuilderExtensions.cs deleted file mode 100644 index 3c7538d0..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using Microsoft.Extensions.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; - -internal static class ConfigurationBuilderExtensions -{ - public const string BasePrefix = "RAD_"; - - public static IConfigurationBuilder AddRadiusConfigurationFile(this IConfigurationBuilder configurationBuilder, RadiusConfigurationFile file) - { - if (file is null) - { - throw new ArgumentNullException(nameof(file)); - } - - configurationBuilder.Add(new XmlAppConfigurationSource(file)); - return configurationBuilder; - } - - public static IConfigurationBuilder AddRadiusEnvironmentVariables(this IConfigurationBuilder configurationBuilder, string configName = null) - { - var preparedConfigName = RadiusConfigurationSource.TransformName(configName); - var prefix = preparedConfigName == string.Empty - ? BasePrefix - : $"{BasePrefix}{preparedConfigName}_"; - configurationBuilder.AddEnvironmentVariables(prefix); - return configurationBuilder; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationExtensions.cs deleted file mode 100644 index 113ba234..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using Microsoft.Extensions.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration; - -public static class ConfigurationExtensions -{ - public static RadiusAdapterConfiguration BindRadiusAdapterConfig(this IConfiguration configuration) - { - if (configuration is null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - return configuration.Get(x => - { - x.BindNonPublicProperties = true; - x.ErrorOnUnknownConfiguration = false; - }); - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/Exceptions/InvalidConfigurationException.cs deleted file mode 100644 index 28f4dc9e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/Exceptions/InvalidConfigurationException.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.ComponentModel; -using System.Linq.Expressions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; - -/// -/// The Radius adapter configuration is invalid. -/// -public class InvalidConfigurationException : Exception -{ - public InvalidConfigurationException(string message) - : base($"Configuration error: {message}") { } - - public InvalidConfigurationException(string message, Exception inner) - : base($"Configuration error: {message}", inner) { } - - protected InvalidConfigurationException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// Returns new for the specified property of a type. You can use a formatted string to pass the property name. - ///
If a attribute is found for the specified property, its value will be passed to the formatted string as argument {prop}. - ///
Otherwise, the real property name will be passed. - ///
Example: - /// - /// - /// class RadiusAdapterConfiguration - /// { - /// [Description("some-property")] - /// public string SomeProperty { get; init; } - /// - /// public int SomeOtherProperty { get; init; } - /// } - /// - /// // InvalidConfigurationException with message "Element 'some-property' not found. Please check configuration file."; - /// InvalidConfigurationException.ThrowFor(x => x.SomeProperty, "Element '{prop}' not found. Please check configuration file."); - /// - /// // InvalidConfigurationException with message "Element 'SomeOtherProperty' has invalid value"; - /// InvalidConfigurationException.ThrowFor(x => x.SomeOtherProperty, "Element '{prop}' has invalid value."); - /// - /// - ///
- /// Property type of a type. - /// Property selector. - /// - /// Formatted message that will be passed to exception. Use pattern {prop} to pass the property name. - ///
You can also use wildcards like {0}, {1}, {n} to replace it with arguments (like method). - /// - /// Items to format message. - /// If is null. - /// If is null, empty or whitespace. - public static InvalidConfigurationException For(Expression> propertySelector, - string formattedMessage, - params object[] args) - { - if (propertySelector is null) - { - throw new ArgumentNullException(nameof(propertySelector)); - } - - if (string.IsNullOrWhiteSpace(formattedMessage)) - { - throw new ArgumentException($"'{nameof(formattedMessage)}' cannot be null or whitespace.", nameof(formattedMessage)); - } - - var propertyName = RadiusAdapterConfigurationDescription.Property(propertySelector); - - formattedMessage = formattedMessage.Replace("{prop}", propertyName); - formattedMessage = string.Format(formattedMessage, args); - - return new InvalidConfigurationException(formattedMessage); - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/IPEndPointFactory.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/IPEndPointFactory.cs deleted file mode 100644 index 57fbe3e7..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/IPEndPointFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.Net; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration -{ - public static class IPEndPointFactory - { - public static bool TryParse(string text, out IPEndPoint ipEndPoint) - { - ipEndPoint = null; - - if (Uri.TryCreate(string.Concat("tcp://", text), UriKind.Absolute, out Uri uri)) - { - if (!IPAddress.TryParse(uri.Host, out var parsed)) return false; - - ipEndPoint = new IPEndPoint(parsed, uri.Port < 0 ? 0 : uri.Port); - return true; - } - - if (Uri.TryCreate(string.Concat("tcp://", string.Concat("[", text, "]")), UriKind.Absolute, out uri)) - { - if (!IPAddress.TryParse(uri.Host, out var parsed)) return false; - - ipEndPoint = new IPEndPoint(parsed, uri.Port < 0 ? 0 : uri.Port); - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationFactory.cs deleted file mode 100644 index dee9eea6..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationFactory.cs +++ /dev/null @@ -1,96 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using Microsoft.Extensions.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; - -/// -/// Creates instance of . -/// -public static class RadiusAdapterConfigurationFactory -{ - /// - /// Tries to read a configuration from file and binds it, then returns an instance of . Also adds environment variables if a configuration name is specified. - /// - /// Configuration file path. - /// Configuration name. - /// Radius Adapter Configuration - /// - /// - /// - public static RadiusAdapterConfiguration Create(RadiusConfigurationFile file, string name = null) - { - if (file is null) - { - throw new ArgumentNullException(nameof(file)); - } - - if (!File.Exists(file)) - { - throw new FileNotFoundException($"Configuration file '{file}' not found"); - } - - var config = new ConfigurationBuilder() - .AddRadiusConfigurationFile(file) - .AddRadiusEnvironmentVariables(name) - .Build(); - - var bounded = config.BindRadiusAdapterConfig(); - if (bounded == null) - { - throw new InvalidOperationException($"Fatal: Unable to bind Radius adapter configuration '{file}'"); - } - - return bounded; - } - - /// - /// Tries to read a configuration from an environment variables with the specified prefix and binds it, then returns an instance of . - /// - /// Instance of . - /// Radius Adapter Configuration - /// - /// - public static RadiusAdapterConfiguration Create(RadiusConfigurationEnvironmentVariable environmentVariable) - { - if (environmentVariable is null) - { - throw new ArgumentNullException(nameof(environmentVariable)); - } - - var config = new ConfigurationBuilder() - .AddRadiusEnvironmentVariables(environmentVariable.Name) - .Build(); - - var bounded = config.BindRadiusAdapterConfig(); - if (bounded == null) - { - throw new InvalidOperationException($"Fatal: Unable to bind Radius adapter configuration '{environmentVariable}'"); - } - - return bounded; - } - - /// - /// Tries to read a common radius configuration from an environment variables and binds it, then returns an instance of . - /// - /// Radius Adapter Configuration - /// - public static RadiusAdapterConfiguration Create() - { - var config = new ConfigurationBuilder() - .AddRadiusEnvironmentVariables() - .Build(); - - var bounded = config.BindRadiusAdapterConfig(); - if (bounded == null) - { - throw new InvalidOperationException("Fatal: Unable to bind Radius adapter root configuration"); - } - - return bounded; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationProvider.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationProvider.cs deleted file mode 100644 index 504234ef..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; - -internal static class RadiusAdapterConfigurationProvider -{ - private static readonly Lazy _rootConfig = new(() => - { - var path = RadiusAdapterConfigurationFile.Path; - var rdsRootConfig = new RadiusConfigurationFile(path); - - // try to read a file... - if (File.Exists(rdsRootConfig)) - { - return RadiusAdapterConfigurationFactory.Create(rdsRootConfig); - } - - // ... and try to read an environment variables otherwise. - return RadiusAdapterConfigurationFactory.Create(); - }); - - public static RadiusAdapterConfiguration GetRootConfiguration() => _rootConfig.Value; -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfiguration.cs deleted file mode 100644 index 4c057fe7..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfiguration.cs +++ /dev/null @@ -1,33 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.Reflection; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -public class RadiusAdapterConfiguration -{ - private static readonly Lazy _knownSectionNames; - public static string[] KnownSectionNames => _knownSectionNames.Value; - - static RadiusAdapterConfiguration() - { - _knownSectionNames = new Lazy(() => - { - return typeof(RadiusAdapterConfiguration) - .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) - .Select(x => x.Name) - .ToArray(); - }); - } - - public AppSettingsSection AppSettings { get; init; } = new(); - public RadiusReplySection RadiusReply { get; init; } = new(); - public UserNameTransformRulesSection UserNameTransformRules { get; init; } = new(); - public LdapServersSection LdapServers { get; init; } = new(); -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationDescription.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationDescription.cs deleted file mode 100644 index b10ae19d..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationDescription.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.ComponentModel; -using System.Linq.Expressions; -using System.Reflection; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter -{ - internal static class RadiusAdapterConfigurationDescription - { - /// - /// Returns description of a Radius adapter configuration property. - ///
If a attribute is found for the specified property, its value will be returned. - ///
Otherwise, the real property name will be returned. - ///
- /// Property type. - /// Property for which you need to get a name. - /// Description of a property - /// if is null or if is null. - /// If you are trying to access a member of a type that is not a property. - public static string Property(Expression> propertySelector) - { - if (propertySelector is null) - { - throw new ArgumentNullException(nameof(propertySelector)); - } - - if (propertySelector.Body is not MemberExpression expression) - { - throw new InvalidOperationException("Only the class property should be selected"); - } - - if (expression.Member is not PropertyInfo property) - { - throw new InvalidOperationException("Only the class property should be selected"); - } - - var attribute = property.GetCustomAttribute(); - if (attribute == null) - { - return property.Name; - } - - var description = attribute.Description; - if (string.IsNullOrWhiteSpace(description)) - { - return property.Name; - } - - return description; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationFile.cs deleted file mode 100644 index 8f1913d8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationFile.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -internal sealed class RadiusAdapterConfigurationFile -{ - private static readonly Lazy _path = new(() => - { - var asm = Assembly.GetAssembly(typeof(RadiusAdapterConfigurationFile)); - if (asm is null) - { - throw new Exception("Unable to get assembly to read build file path"); - } - - return $"{asm.Location}.config"; - }); - - public static string ConfigName => System.IO.Path.GetFileNameWithoutExtension(_path.Value); - - public static string Path => _path.Value; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/AppSettingsSection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/AppSettingsSection.cs deleted file mode 100644 index 9785fe54..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/AppSettingsSection.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.ComponentModel; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections; - -public class AppSettingsSection -{ - [Description("multifactor-api-url")] - public string MultifactorApiUrl { get; init; } = string.Empty; - - [Description("multifactor-api-proxy")] - public string MultifactorApiProxy { get; init; } = string.Empty; - - [Description("multifactor-api-timeout")] - public string MultifactorApiTimeout { get; init; } = string.Empty; - - [Description("multifactor-nas-identifier")] - public string MultifactorNasIdentifier { get; init; } = string.Empty; - - [Description("multifactor-shared-secret")] - public string MultifactorSharedSecret { get; init; } = string.Empty; - - [Description("sign-up-groups")] - public string SignUpGroups { get; init; } = string.Empty; - - [Description("bypass-second-factor-when-api-unreachable")] - public bool BypassSecondFactorWhenApiUnreachable { get; init; } = true; - - [Description("first-factor-authentication-source")] - public string FirstFactorAuthenticationSource { get; init; } = string.Empty; - - [Description("adapter-client-endpoint")] - public string AdapterClientEndpoint { get; init; } = string.Empty; - - [Description("adapter-server-endpoint")] - public string AdapterServerEndpoint { get; init; } = string.Empty; - - [Description("nps-server-endpoint")] - public string NpsServerEndpoint { get; init; } = string.Empty; - - [Description("nps-server-timeout")] - public string NpsServerTimeout { get; init; } = "00:00:05"; - - [Description("radius-client-ip")] - public string RadiusClientIp { get; init; } = string.Empty; - - [Description("radius-client-nas-identifier")] - public string RadiusClientNasIdentifier { get; init; } = string.Empty; - - [Description("radius-shared-secret")] - public string RadiusSharedSecret { get; init; } = string.Empty; - - [Description("privacy-mode")] - public string PrivacyMode { get; init; } = string.Empty; - - [Description("pre-authentication-method")] - public string PreAuthenticationMethod { get; init; } = string.Empty; - - [Description("authentication-cache-lifetime")] - public string AuthenticationCacheLifetime { get; init; } = string.Empty; - - [Description("invalid-credential-delay")] - public string InvalidCredentialDelay { get; init; } = string.Empty; - - [Description("logging-format")] - public string LoggingFormat { get; init; } = string.Empty; - - [Description("logging-level")] - public string LoggingLevel { get; init; } = string.Empty; - - [Description("calling-station-id-attribute")] - public string CallingStationIdAttribute { get; init; } = string.Empty; - - [Description("console-log-output-template")] - public string ConsoleLogOutputTemplate { get; init; } = string.Empty; - - [Description("file-log-output-template")] - public string FileLogOutputTemplate { get; init; } = string.Empty; - - [Description("ip-white-list")] - public string IpWhiteList { get; init; } = string.Empty; - - [Description("syslog-server")] - public string SyslogServer { get; init; } = string.Empty; - [Description("syslog-format")] - public string SyslogFormat { get; init; } = string.Empty; - [Description("syslog-facility")] - public string SyslogFacility { get; init; } = string.Empty; - [Description("syslog-app-name")] - public string SyslogAppName { get; init; } = "multifactor-radius"; - [Description("log-file-max-size-bytes")] - public int LogFileMaxSizeBytes { get; init; } = 1073741824; - [Description("syslog-use-tls")] - public bool SyslogUseTls { get; init; } = false; - [Description("syslog-framer")] - public string SyslogFramer { get; init; } = string.Empty; - [Description("syslog-output-template")] - public string SyslogOutputTemplate { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServerConfiguration.cs deleted file mode 100644 index 71f07258..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServerConfiguration.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.ComponentModel; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; - -public class LdapServerConfiguration -{ - [Description("connection-string")] - public string ConnectionString { get; init; } = string.Empty; - - [Description("username")] - public string UserName { get; init; } = string.Empty; - - [Description("password")] - public string Password { get; init; } = string.Empty; - - [Description("bind-timeout-in-seconds")] - public int BindTimeoutInSeconds { get; init; } = 30; - - [Description("access-groups")] - public string AccessGroups { get; init; } = string.Empty; - - [Description("second-fa-groups")] - public string SecondFaGroups { get; init; } = string.Empty; - - [Description("second-fa-bypass-groups")] - public string SecondFaBypassGroups { get; init; } = string.Empty; - - [Description("load-nested-groups")] - public bool LoadNestedGroups { get; init; } = true; - - [Description("nested-groups-base-dn")] - public string NestedGroupsBaseDn { get; init; } = string.Empty; - - [Description("phone-attributes")] - public string PhoneAttributes { get; init; } = string.Empty; - - [Description("ip-white-list")] - public string IpWhiteList { get; init; } = string.Empty; - - [Description("authentication-cache-groups")] - public string AuthenticationCacheGroups { get; init; } = string.Empty; - - [Description("identity-attribute")] - public string IdentityAttribute { get; init; } = string.Empty; - - [Description("requires-upn")] - public bool RequiresUpn { get; init; } = false; - - [Description("enable-trusted-domains")] - public bool EnableTrustedDomains { get; init; } = false; - - [Description("included-domains")] - public string IncludedDomains { get; set; } = string.Empty; - - [Description("excluded-domains")] - public string ExcludedDomains { get; set; } = string.Empty; - - [Description("enable-alternative-suffixes")] - public bool EnableAlternativeSuffixes { get; init; } = false; - - [Description("included-suffixes")] - public string IncludedSuffixes { get; set; } = string.Empty; - - [Description("excluded-suffixes")] - public string ExcludedSuffixes { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServersSection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServersSection.cs deleted file mode 100644 index 9b515545..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServersSection.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.ComponentModel; -using Microsoft.Extensions.Configuration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.LdapServer; - -[Description("LdapServers")] -public class LdapServersSection -{ - [ConfigurationKeyName("LdapServer")] - public LdapServerConfiguration?[] LdapServers { get; set; } = []; - - [ConfigurationKeyName("LdapServer")] - public LdapServerConfiguration? LdapServer { get; set; } = null; - - public LdapServerConfiguration[] Servers - { - get - { - //because .net always binds empty object instead of null - if (!string.IsNullOrWhiteSpace(LdapServer?.ConnectionString)) - { - return [LdapServer]; - } - - var configs = new List(); - foreach (var config in LdapServers) - { - if (config != null) - configs.Add(config); - } - - return configs.ToArray(); - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttribute.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttribute.cs deleted file mode 100644 index 9c13c466..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; - -public class RadiusReplyAttribute -{ - public string Name { get; init; } = string.Empty; - public string Value { get; init; } = string.Empty; - public string When { get; init; } = string.Empty; - public string From { get; init; } = string.Empty; - public bool Sufficient { get; init; } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttributesSection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttributesSection.cs deleted file mode 100644 index feccc882..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttributesSection.cs +++ /dev/null @@ -1,48 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.ComponentModel; -using Microsoft.Extensions.Configuration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; - -[Description("Attributes")] -public class RadiusReplyAttributesSection -{ - [ConfigurationKeyName("add")] - private RadiusReplyAttribute[] _elements { get; set; } - - [ConfigurationKeyName("add")] - private RadiusReplyAttribute _singleElement { get; set; } - - public RadiusReplyAttributesSection() - { - } - - public RadiusReplyAttributesSection(RadiusReplyAttribute singleElement = null, RadiusReplyAttribute[] elements = null) - { - _elements = elements; - _singleElement = singleElement; - } - - public RadiusReplyAttribute[] Elements - { - get - { - // To deal with a single element binding to array issue, we should map a single claim manually - // See: https://github.com/dotnet/runtime/issues/57325 - if (!string.IsNullOrWhiteSpace(_singleElement?.Name)) - { - return new [] { _singleElement }; - } - - if (_elements != null && _elements.All(x => !string.IsNullOrWhiteSpace(x.Name))) - { - return _elements; - } - - return Array.Empty(); - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplySection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplySection.cs deleted file mode 100644 index 1a6f651a..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplySection.cs +++ /dev/null @@ -1,10 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.RadiusReply; - -public class RadiusReplySection -{ - public RadiusReplyAttributesSection Attributes { get; init; } = new(); -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRule.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRule.cs deleted file mode 100644 index 4b9888ad..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRule.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -public class UserNameTransformRule -{ - public string Match { get; init; } = string.Empty; - public string Replace { get; init; } = string.Empty; - public int Count { get; init; } = 0; -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesCollection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesCollection.cs deleted file mode 100644 index bd37f3a9..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesCollection.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -public class UserNameTransformRulesCollection -{ - [ConfigurationKeyName("add")] - private UserNameTransformRule[] _elements { get; set; } - - [ConfigurationKeyName("add")] - private UserNameTransformRule _singleElement { get; set; } - - public UserNameTransformRule[] Elements - { - get - { - // To deal with a single element binding to array issue, we should map a single claim manually - // See: https://github.com/dotnet/runtime/issues/57325 - var hasSingle = !string.IsNullOrWhiteSpace(_singleElement?.Match) || - !string.IsNullOrWhiteSpace(_singleElement?.Replace); - if (hasSingle) - { - return new[] { _singleElement }; - } - - if (_elements != null) - { - return _elements; - } - - return Array.Empty(); - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesSection.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesSection.cs deleted file mode 100644 index 9fba9e00..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesSection.cs +++ /dev/null @@ -1,11 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -public class UserNameTransformRulesSection : UserNameTransformRulesCollection -{ - public UserNameTransformRulesCollection BeforeFirstFactor { get; init; } = new(); - public UserNameTransformRulesCollection BeforeSecondFactor { get; init; } = new(); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformSettings.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformSettings.cs deleted file mode 100644 index ebe690a8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformSettings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Sections.UserNameTransform; - -public class UserNameTransformSettings -{ - [ConfigurationKeyName("add")] - private UserNameTransformRule[] _elements { get; set; } - - public UserNameTransformRule[] Elements - { - get - { - return _elements; - } - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationEnvironmentVariable.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationEnvironmentVariable.cs deleted file mode 100644 index 838be030..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationEnvironmentVariable.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -public sealed class RadiusConfigurationEnvironmentVariable : RadiusConfigurationSource -{ - public override string Name { get; } - - public RadiusConfigurationEnvironmentVariable(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - } - - Name = name; - } - - public override string ToString() => Name; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs deleted file mode 100644 index 3d12fc2b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs +++ /dev/null @@ -1,73 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -/// -/// Describes a Radius Adapter configuration file. -/// -public sealed class RadiusConfigurationFile : RadiusConfigurationSource -{ - /// - /// Configuration file path. - /// - public string Path { get; } - - /// - /// Configuration file name without extension. - /// - public override string Name { get; } - - /// - /// Configuration file name with extension. - /// - public string FileName { get; } - - public RadiusConfigurationFile(string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException($"'{nameof(path)}' cannot be null or whitespace.", nameof(path)); - } - - if (path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) != -1) - { - throw new ArgumentException("Invalid configuration path", nameof(path)); - } - - var name = System.IO.Path.GetFileName(path); - if (!name.EndsWith(".config")) - { - throw new ArgumentException("Invalid configuration path", nameof(path)); - } - - Path = path; - Name = System.IO.Path.GetFileNameWithoutExtension(path); - FileName = System.IO.Path.GetFileName(path); - } - - 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.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationSource.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationSource.cs deleted file mode 100644 index 87fbe1c2..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationSource.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -public abstract class RadiusConfigurationSource -{ - /// - /// Source name. - /// - public abstract string Name { get; } - - public override bool Equals(object obj) - { - if (obj is not RadiusConfigurationSource rad) - { - return false; - } - - if (ReferenceEquals(obj, this)) - { - return true; - } - - return Name == rad.Name; - } - - public override int GetHashCode() => Name.GetHashCode(); - - public static string TransformName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - return string.Empty; - } - - name = Regex.Replace(name, @"\s+", string.Empty); - return name; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAppConfigurationSource.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAppConfigurationSource.cs deleted file mode 100644 index 39f4241c..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAppConfigurationSource.cs +++ /dev/null @@ -1,122 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.Xml.Linq; -using Microsoft.Extensions.Configuration; -using Multifactor.Radius.Adapter.v2.Extensions; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -public class XmlAppConfigurationSource : ConfigurationProvider, IConfigurationSource -{ - private const string _appSettingsElement = "appSettings"; - - private readonly RadiusConfigurationFile _path; - - public XmlAppConfigurationSource(RadiusConfigurationFile path) - { - _path = path ?? throw new ArgumentNullException(nameof(path)); - } - - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - - public override void Load() - { - try - { - LoadInternal(); - } - catch (Exception ex) - { - throw new Exception($"Failed to load configuration file '{_path.Path}'", ex); - } - } - - private void LoadInternal() - { - var xml = XDocument.Load(_path); - var root = xml.Root; - - if (root is null) - { - throw new Exception("Root XML element not found"); - } - - var appSettings = root.Element(_appSettingsElement); - if (appSettings != null) - { - var appSettingsElements = appSettings.Elements().ToArray(); - XmlAssert.HasUniqueElements(appSettingsElements, x => x.Attribute("key")?.Value); - - FillAppSettingsSection(appSettingsElements); - } - - var sections = root.Elements() - .Where(x => x.Name != _appSettingsElement) - .ToArray(); - XmlAssert.HasUniqueElements(sections, x => x.Name); - - foreach (var section in sections) - { - FillSection(section); - } - } - - private void FillAppSettingsSection(XElement[] appSettingsElements) - { - for (var i = 0; i < appSettingsElements.Length; i++) - { - var key = XmlAssert.HasAttribute(appSettingsElements[i], "key"); - var value = XmlAssert.HasAttribute(appSettingsElements[i], "value"); - - var newKey = $"{_appSettingsElement}:{key.ToPascalCase()}"; - Data.Add(newKey, value); - } - } - - private void FillSection(XElement section, string parentKey = null, string postfix = null) - { - var sectionKey = section.Name.ToString(); - if (parentKey != null) - { - sectionKey = $"{parentKey}:{sectionKey}"; - } - - if (postfix != null) - { - sectionKey = $"{sectionKey}:{postfix}"; - } - - if (section.HasAttributes) - { - foreach (var attr in section.Attributes()) - { - var attrKey = $"{sectionKey}:{attr.Name.LocalName.ToPascalCase()}"; - Data[attrKey] = attr.Value; - } - } - - if (!section.HasElements) - { - return; - } - - var groups = section.Elements().GroupBy(x => x.Name); - foreach (var group in groups) - { - if (group.Count() == 1) - { - FillSection(group.First(), sectionKey); - continue; - } - - var index = 0; - foreach (var arrEntry in group) - { - FillSection(arrEntry, sectionKey, index.ToString()); - index++; - } - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAssert.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAssert.cs deleted file mode 100644 index 8489e79e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAssert.cs +++ /dev/null @@ -1,73 +0,0 @@ -//Copyright(c) 2020 MultiFactor -//Please see licence at -//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md - -using System.Xml.Linq; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - -internal static class XmlAssert -{ - /// - /// Explodes if the collection contains duplicates. - /// - /// Selector key type. - /// Source collection. - /// Grouping selector. - /// - /// - public static void HasUniqueElements(IEnumerable elements, Func keySelector) - { - if (elements is null) - { - throw new ArgumentNullException(nameof(elements)); - } - - if (keySelector is null) - { - throw new ArgumentNullException(nameof(keySelector)); - } - - var duplicates = elements - .GroupBy(keySelector) - .Where(x => x.Count() > 1) - .Select(x => $"'{x.Key}'") - .ToArray(); - - if (duplicates.Length != 0) - { - var d = string.Join(", ", duplicates); - throw new Exception($"Invalid xml config. Duplicates found: {d}"); - } - } - - /// - /// Returns attribute value or throws if the attribute does not exist. - /// - /// Target element. - /// Attribute to get value from. - /// - /// - /// - /// - public static string HasAttribute(XElement element, string attribute) - { - if (element is null) - { - throw new ArgumentNullException(nameof(element)); - } - - if (string.IsNullOrWhiteSpace(attribute)) - { - throw new ArgumentException($"'{nameof(attribute)}' cannot be null or whitespace.", nameof(attribute)); - } - - var attr = element.Attribute(attribute); - if (attr == null) - { - throw new Exception($"Invalid xml config: required attribute 'value' not found. Target element: {element}"); - } - - return attr.Value; - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/MultifactorHttpClient.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/MultifactorHttpClient.cs deleted file mode 100644 index a8849767..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/MultifactorHttpClient.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; - -public interface IHttpClient -{ - Task PostAsync(string endpoint, object body, IReadOnlyDictionary? headers = null); -} - -public class MultifactorHttpClient : IHttpClient -{ - private readonly IHttpClientFactory _factory; - private readonly ILogger _logger; - - public MultifactorHttpClient(IHttpClientFactory factory, ILogger logger) - { - _factory = factory; - _logger = logger; - } - - public async Task PostAsync(string endpoint, object? body, IReadOnlyDictionary? headers = null) - { - ArgumentNullException.ThrowIfNull(endpoint); - - var request = new HttpRequestMessage(HttpMethod.Post, endpoint) - { - Content = body == null ? null : CreateJsonStringContent(body) - }; - - AddHeaders(request, headers); - - var cli = _factory.CreateClient(nameof(MultifactorHttpClient)); - _logger.LogDebug("Sending request to API: {@payload}", body); - - using var response = await cli.SendAsync(request); - - response.EnsureSuccessStatusCode(); - - var parsed = await DeserializeAsync(response.Content); - _logger.LogDebug("Received response from API: {@response}", parsed); - - return parsed; - } - - private static void AddHeaders(HttpRequestMessage message, IReadOnlyDictionary? headers) - { - if (headers == null) - { - return; - } - - foreach (var h in headers) - { - message.Headers.Add(h.Key, h.Value); - } - } - - private static StringContent CreateJsonStringContent(object data) - { - var payload = JsonSerializer.Serialize(data, GetJsonSerializerOptions()); - return new StringContent(payload, Encoding.UTF8, "application/json"); - } - - private static async Task DeserializeAsync(HttpContent content) - { - var jsonResponse = await content.ReadAsStringAsync(); - var parsed = JsonSerializer.Deserialize(jsonResponse, GetJsonSerializerOptions()); - return parsed; - } - - private static JsonSerializerOptions GetJsonSerializerOptions() - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never - }; - options.Converters.Add(new JsonStringEnumConverter()); - - return options; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/IPipelineBuilder.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/IPipelineBuilder.cs deleted file mode 100644 index 5e1e7e53..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/IPipelineBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; - -public interface IPipelineBuilder -{ - public IPipelineBuilder AddPipelineStep(IRadiusPipelineStep step); - IRadiusPipeline? Build(); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/PipelineBuilder.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/PipelineBuilder.cs deleted file mode 100644 index 25072fe5..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/PipelineBuilder.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; - -public class PipelineBuilder : IPipelineBuilder -{ - private readonly List _pipelineSteps = new(); - - public IPipelineBuilder AddPipelineStep(IRadiusPipelineStep step) - { - _pipelineSteps.Add(step); - return this; - } - - public IRadiusPipeline Build() - { - var nextStep = new RadiusPipeline(); - if (_pipelineSteps.Count == 0) - return nextStep; - - RadiusPipeline? pipeline = null; - for (int i = _pipelineSteps.Count - 1; i >= 0; i--) - { - pipeline = new RadiusPipeline(currentStep: _pipelineSteps[i], nextStep: nextStep); - nextStep = pipeline; - } - - return pipeline!; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineConfigurationFactory.cs deleted file mode 100644 index c7cdb59f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineConfigurationFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public interface IPipelineConfigurationFactory -{ - public PipelineConfiguration CreatePipelineConfiguration(IPipelineStepsConfiguration pipelineStepsConfiguration); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineStepsConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineStepsConfiguration.cs deleted file mode 100644 index f9cdafa8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineStepsConfiguration.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public interface IPipelineStepsConfiguration -{ - public string ConfigurationName { get; } - public PreAuthMode PreAuthMode { get; } - bool ShouldLoadUserGroups { get; } - public bool HasLdapServers { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfiguration.cs deleted file mode 100644 index 9ad5fc7f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public class PipelineConfiguration -{ - public Type[] PipelineStepsTypes { get; } - - public PipelineConfiguration(Type[] pipelineStepsTypes) - { - if (pipelineStepsTypes is null) - throw new ArgumentNullException(nameof(pipelineStepsTypes)); - PipelineStepsTypes = pipelineStepsTypes; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfigurationFactory.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfigurationFactory.cs deleted file mode 100644 index fde63419..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfigurationFactory.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Services.Cache; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public class PipelineConfigurationFactory : IPipelineConfigurationFactory -{ - private readonly ICacheService _cache; - - public PipelineConfigurationFactory(ICacheService cache) - { - _cache = cache; - } - - public PipelineConfiguration CreatePipelineConfiguration(IPipelineStepsConfiguration pipelineStepsConfiguration) - { - var existedPipeline = GetExistedPipeline(pipelineStepsConfiguration.ConfigurationName); - if (existedPipeline != null) - { - return existedPipeline; - } - - PipelineConfiguration newPipeline = BuildNewPipeline(pipelineStepsConfiguration); - _cache.Set(pipelineStepsConfiguration.ConfigurationName, newPipeline); - return newPipeline; - } - - private PipelineConfiguration? GetExistedPipeline(string pipelineName) - { - if (!_cache.TryGetValue(pipelineName, out PipelineConfiguration? pipeline)) - return null; - - return pipeline; - } - - private PipelineConfiguration BuildNewPipeline(IPipelineStepsConfiguration pipelineStepsConfiguration) - { - return pipelineStepsConfiguration.HasLdapServers ? GetPipelineWithLdap(pipelineStepsConfiguration) : GetPipelineWithoutLdap(pipelineStepsConfiguration); - } - - private PipelineConfiguration GetPipelineWithoutLdap(IPipelineStepsConfiguration pipelineStepsConfiguration) - { - var pipeline = new List(); - - pipeline.Add(typeof(StatusServerFilteringStep)); - pipeline.Add(typeof(IpWhiteListStep)); - pipeline.Add(typeof(AccessRequestFilteringStep)); - pipeline.Add(typeof(AccessChallengeStep)); - - if (pipelineStepsConfiguration.PreAuthMode != PreAuthMode.None) - { - pipeline.Add(typeof(PreAuthCheckStep)); - pipeline.Add(typeof(SecondFactorStep)); - pipeline.Add(typeof(PreAuthPostCheck)); - pipeline.Add(typeof(FirstFactorStep)); - } - else - { - pipeline.Add(typeof(FirstFactorStep)); - pipeline.Add(typeof(SecondFactorStep)); - } - - return new PipelineConfiguration(pipeline.ToArray()); - } - - private PipelineConfiguration GetPipelineWithLdap(IPipelineStepsConfiguration pipelineStepsConfiguration) - { - var pipeline = new List(); - - pipeline.Add(typeof(StatusServerFilteringStep)); - pipeline.Add(typeof(IpWhiteListStep)); - pipeline.Add(typeof(AccessRequestFilteringStep)); - pipeline.Add(typeof(UserNameValidationStep)); - pipeline.Add(typeof(LdapSchemaLoadingStep)); - pipeline.Add(typeof(ProfileLoadingStep)); - pipeline.Add(typeof(AccessGroupsCheckingStep)); - pipeline.Add(typeof(AccessChallengeStep)); - - if (pipelineStepsConfiguration.PreAuthMode != PreAuthMode.None) - { - pipeline.Add(typeof(PreAuthCheckStep)); - pipeline.Add(typeof(SecondFactorStep)); - pipeline.Add(typeof(PreAuthPostCheck)); - pipeline.Add(typeof(FirstFactorStep)); - } - else - { - pipeline.Add(typeof(FirstFactorStep)); - pipeline.Add(typeof(SecondFactorStep)); - } - - if (pipelineStepsConfiguration.ShouldLoadUserGroups) - pipeline.Add(typeof(UserGroupLoadingStep)); - - return new PipelineConfiguration(pipeline.ToArray()); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineStepsConfiguration.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineStepsConfiguration.cs deleted file mode 100644 index 3a378ee1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineStepsConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; - -public class PipelineStepsConfiguration : IPipelineStepsConfiguration -{ - public string ConfigurationName { get; } - - public PreAuthMode PreAuthMode { get; } - - public bool ShouldLoadUserGroups { get; } - - // TODO Maybe use something better - public bool HasLdapServers { get; } - - public PipelineStepsConfiguration(string configurationName, PreAuthMode preAuthMode, bool shouldLoadGroups = false, bool hasLdapServers = false) - { - if (string.IsNullOrWhiteSpace(configurationName)) - throw new ArgumentException($"'{nameof(configurationName)}' cannot be null or whitespace.", nameof(configurationName)); - - ConfigurationName = configurationName; - PreAuthMode = preAuthMode; - ShouldLoadUserGroups = shouldLoadGroups; - HasLdapServers = hasLdapServers; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/IRadiusPipelineExecutionContext.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/IRadiusPipelineExecutionContext.cs deleted file mode 100644 index e20a8852..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/IRadiusPipelineExecutionContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Net; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -public interface IRadiusPipelineExecutionContext -{ - ILdapProfile? UserLdapProfile { get; set; } - IRadiusPacket RequestPacket { get; } - IRadiusPacket? ResponsePacket { get; set; } - IAuthenticationState AuthenticationState { get; set; } - IResponseInformation ResponseInformation { get; set; } - IExecutionState ExecutionState { get; } - string? MustChangePasswordDomain { get; set; } - IPEndPoint RemoteEndpoint { get; } - IPEndPoint? ProxyEndpoint { get; } - ILdapSchema? LdapSchema { get; set; } - UserPassphrase Passphrase { get; set; } - HashSet UserGroups { get; set; } - ILdapServerConfiguration? LdapServerConfiguration { get; } - AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; } - bool BypassSecondFactorWhenApiUnreachable { get; } - AuthenticationSource FirstFactorAuthenticationSource { get; } - ApiCredential ApiCredential { get; } - IReadOnlySet NpsServerEndpoints { get; } - TimeSpan NpsServerTimeout { get; } - PrivacyModeDescriptor PrivacyMode { get; } - IReadOnlyDictionary RadiusReplyAttributes { get; } - IPEndPoint ServiceClientEndpoint { get; } - string SignUpGroups { get; } - UserNameTransformRules UserNameTransformRules { get; } - RandomWaiterConfig InvalidCredentialDelay { get; } - PreAuthModeDescriptor PreAuthnMode { get; } - string ClientConfigurationName { get; } - SharedSecret RadiusSharedSecret { get; } - IReadOnlyCollection IpWhiteList { get; } - IReadOnlyList ApiUrls { get; } - bool IsDomainAccount { get; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/RadiusPipelineExecutionContext.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/RadiusPipelineExecutionContext.cs deleted file mode 100644 index e6140c93..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/RadiusPipelineExecutionContext.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Net; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Pipeline.Settings; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -public class RadiusPipelineExecutionContext : IRadiusPipelineExecutionContext -{ - private readonly IPipelineExecutionSettings _settings; - public ILdapProfile? UserLdapProfile { get; set; } - public IRadiusPacket RequestPacket { get; } - public IRadiusPacket? ResponsePacket { get; set; } - public IExecutionState ExecutionState { get; } = new ExecutionState(); - public IAuthenticationState AuthenticationState { get; set; } = new AuthenticationState(); - public IResponseInformation ResponseInformation { get; set; } = new ResponseInformation(); - public string MustChangePasswordDomain { get; set; } - public IPEndPoint RemoteEndpoint => RequestPacket.RemoteEndpoint; - public IPEndPoint? ProxyEndpoint => RequestPacket.ProxyEndpoint; - public ILdapSchema? LdapSchema { get; set; } - public UserPassphrase Passphrase { get; set; } - public HashSet UserGroups { get; set; } = new(); - public ILdapServerConfiguration? LdapServerConfiguration => _settings.LdapServerConfiguration; - public AuthenticatedClientCacheConfig AuthenticationCacheLifetime => _settings.AuthenticationCacheLifetime; - public bool BypassSecondFactorWhenApiUnreachable => _settings.BypassSecondFactorWhenApiUnreachable; - public AuthenticationSource FirstFactorAuthenticationSource => _settings.FirstFactorAuthenticationSource; - public ApiCredential ApiCredential => _settings.ApiCredential; - public IReadOnlySet NpsServerEndpoints => _settings.NpsServerEndpoints; - public TimeSpan NpsServerTimeout => _settings.NpsServerTimeout; - public PrivacyModeDescriptor PrivacyMode => _settings.PrivacyMode; - public IReadOnlyDictionary RadiusReplyAttributes => _settings.RadiusReplyAttributes; - public IPEndPoint ServiceClientEndpoint => _settings.ServiceClientEndpoint; - public string SignUpGroups => _settings.SignUpGroups; - public UserNameTransformRules UserNameTransformRules => _settings.UserNameTransformRules; - public RandomWaiterConfig InvalidCredentialDelay => _settings.InvalidCredentialDelay; - public PreAuthModeDescriptor PreAuthnMode => _settings.PreAuthnMode; - public string ClientConfigurationName => _settings.ClientConfigurationName; - public SharedSecret RadiusSharedSecret => _settings.RadiusSharedSecret; - public IReadOnlyCollection IpWhiteList => _settings.LdapServerConfiguration?.IpWhiteList.Count > 0 ? _settings.LdapServerConfiguration.IpWhiteList : _settings.IpWhiteList; - public IReadOnlyList ApiUrls => _settings.ApiUrls; - public bool IsDomainAccount => RequestPacket.AccountType == AccountType.Domain; - - public RadiusPipelineExecutionContext(IPipelineExecutionSettings settings, IRadiusPacket requestPacket) - { - Throw.IfNull(settings, nameof(settings)); - Throw.IfNull(requestPacket, nameof(requestPacket)); - - _settings = settings; - RequestPacket = requestPacket; - } - -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IPipelineProvider.cs deleted file mode 100644 index 51daa81d..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IPipelineProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public interface IPipelineProvider -{ - IRadiusPipeline? GetRadiusPipeline(string key); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IRadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IRadiusPipeline.cs deleted file mode 100644 index f101788e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IRadiusPipeline.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public interface IRadiusPipeline -{ - Task ExecuteAsync(IRadiusPipelineExecutionContext context); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/PipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/PipelineProvider.cs deleted file mode 100644 index 7bb59120..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/PipelineProvider.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public class PipelineProvider : IPipelineProvider -{ - private readonly Dictionary _pipelines = new(); - - public PipelineProvider(IServiceConfiguration configuration, IPipelineConfigurationFactory pipelineConfigurationFactory, IServiceProvider serviceProvider, ILogger logger) - { - Throw.IfNull(configuration, nameof(configuration)); - Throw.IfNull(pipelineConfigurationFactory, nameof(pipelineConfigurationFactory)); - Throw.IfNull(serviceProvider, nameof(serviceProvider)); - - logger.LogDebug($"Initializing pipelines."); - - foreach (var clientConfiguration in configuration.Clients) - { - var shouldLoadUserGroups = ShouldLoadUserGroups(clientConfiguration); - var hasLdapServers = clientConfiguration.LdapServers.Count > 0; - var pipelineSettings = new PipelineStepsConfiguration(clientConfiguration.Name, clientConfiguration.PreAuthnMode.Mode, shouldLoadUserGroups, hasLdapServers); - var pipelineConfig = pipelineConfigurationFactory.CreatePipelineConfiguration(pipelineSettings); - var pipeline = BuildPipeline(pipelineConfig, serviceProvider); - var log = BuildLog(clientConfiguration.Name, pipelineConfig); - _pipelines.TryAdd(clientConfiguration.Name, pipeline); - logger.LogDebug(log); - } - } - - public IRadiusPipeline? GetRadiusPipeline(string key) - { - return _pipelines[key]; - } - - private IRadiusPipeline BuildPipeline(PipelineConfiguration pipelineConfiguration, IServiceProvider serviceProvider) - { - foreach (var stepType in pipelineConfiguration.PipelineStepsTypes) - { - if (!typeof(IRadiusPipelineStep).IsAssignableFrom(stepType)) - { - throw new ArgumentException( - $"The type {stepType.FullName} does not implement {nameof(IRadiusPipelineStep)}"); - } - } - - var builder = new PipelineBuilder(); - foreach (var type in pipelineConfiguration.PipelineStepsTypes) - { - var step = (IRadiusPipelineStep)serviceProvider.GetRequiredService(type); - builder.AddPipelineStep(step); - } - - return builder.Build()!; - } - - private string BuildLog(string configName, PipelineConfiguration pipelineConfiguration) - { - var builder = new StringBuilder(); - builder.AppendLine($"Configuration: {configName}"); - builder.AppendLine("Steps:"); - for (int i = 0; i < pipelineConfiguration.PipelineStepsTypes.Length; i++) - { - builder.AppendLine($"{i+1}. {pipelineConfiguration.PipelineStepsTypes[i].Name}"); - } - - return builder.ToString(); - } - - private bool ShouldLoadUserGroups(IClientConfiguration config) => config - .RadiusReplyAttributes - .Values - .SelectMany(x => x) - .Any(x => x.IsMemberOf || x.UserGroupCondition.Count > 0); - -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/RadiusPipeline.cs deleted file mode 100644 index 83a581c1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/RadiusPipeline.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; - -public class RadiusPipeline : IRadiusPipeline -{ - private readonly IRadiusPipelineStep? _currentStep; - private readonly IRadiusPipeline? _nextStep; - - public RadiusPipeline(IRadiusPipelineStep? currentStep = null, IRadiusPipeline? nextStep = null) - { - _currentStep = currentStep; - _nextStep = nextStep; - } - - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - if (_currentStep is not null) - await _currentStep.ExecuteAsync(context); - - if (context.ExecutionState.IsTerminated) - return; - - if (_nextStep is not null) - await _nextStep.ExecuteAsync(context); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessGroupsCheckingStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessGroupsCheckingStep.cs deleted file mode 100644 index 691cf828..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessGroupsCheckingStep.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public class AccessGroupsCheckingStep : IRadiusPipelineStep -{ - private readonly ILdapGroupService _ldapGroupService; - private readonly ILogger _logger; - - public AccessGroupsCheckingStep( - ILdapGroupService ldapGroupService, - ILogger logger) - { - _ldapGroupService = ldapGroupService; - _logger = logger; - } - - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _logger.LogDebug("'{name}' started", nameof(AccessGroupsCheckingStep)); - ArgumentNullException.ThrowIfNull(context, nameof(context)); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration, nameof(context.LdapServerConfiguration)); - ArgumentNullException.ThrowIfNull(context.LdapSchema, nameof(context.LdapSchema)); - - var serverConfig = context.LdapServerConfiguration; - - if (ShouldSkipStep(context)) - return Task.CompletedTask; - - ArgumentNullException.ThrowIfNull(context.UserLdapProfile, nameof(context.UserLdapProfile)); - - var accessGroupsDns = serverConfig.AccessGroups.ToArray(); - var request = GetMembershipRequest(context, accessGroupsDns); - var isMember = _ldapGroupService.IsMemberOf(request); - - return isMember ? Task.CompletedTask : TerminatePipeline(context); - } - - private MembershipRequest GetMembershipRequest(IRadiusPipelineExecutionContext context, - DistinguishedName[] accessGroupNames) => new(context, accessGroupNames); - - private Task TerminatePipeline(IRadiusPipelineExecutionContext context) - { - _logger.LogWarning("User '{user}' is not member of any access group of the '{connectionString}'.", - context.UserLdapProfile!.Dn, context.LdapServerConfiguration!.ConnectionString); - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - context.ExecutionState.Terminate(); - return Task.CompletedTask; - } - - private bool ShouldSkipStep(IRadiusPipelineExecutionContext context) - { - return NoAccessGroups(context) || UnsupportedAccountType(context); - } - - private bool NoAccessGroups(IRadiusPipelineExecutionContext config) - { - var noGroups = config.LdapServerConfiguration!.AccessGroups.Count == 0; - - if (!noGroups) - return false; - - _logger.LogDebug("No access groups were specified."); - return true; - } - - private bool UnsupportedAccountType(IRadiusPipelineExecutionContext context) - { - if (context.IsDomainAccount) - return false; - - var packet = context.RequestPacket; - _logger.LogInformation( - "User '{user}' used '{accountType}' account to log in. Access groups checking is skipped.", - packet.UserName, - packet.AccountType); - - return true; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessRequestFilteringStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessRequestFilteringStep.cs deleted file mode 100644 index 11fd564b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessRequestFilteringStep.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public class AccessRequestFilteringStep : IRadiusPipelineStep -{ - private readonly ILogger _logger; - public AccessRequestFilteringStep(ILogger logger) - { - _logger = logger; - } - - public async Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _logger.LogDebug("'{name}' started", nameof(AccessRequestFilteringStep)); - if (context.RequestPacket.Code == PacketCode.AccessRequest) - { - await Task.CompletedTask; - return; - } - - var client = context.ProxyEndpoint?.Address ?? context.RemoteEndpoint.Address; - _logger.LogWarning("Unprocessable packet type: {code:l}, from {client:l}", context.RequestPacket.Code.ToString(), client.ToString()); - context.ExecutionState.Terminate(); - context.ExecutionState.SkipResponse(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IRadiusPipelineStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IRadiusPipelineStep.cs deleted file mode 100644 index e6e7c25b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IRadiusPipelineStep.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public interface IRadiusPipelineStep -{ - Task ExecuteAsync(IRadiusPipelineExecutionContext context); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/LdapSchemaLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/LdapSchemaLoadingStep.cs deleted file mode 100644 index 278e5b2e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/LdapSchemaLoadingStep.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public class LdapSchemaLoadingStep: IRadiusPipelineStep -{ - private readonly ILdapSchemaLoader _ldapSchemaLoader; - private readonly ICacheService _cache; - private readonly ILogger _logger; - - public LdapSchemaLoadingStep(ILdapSchemaLoader ldapSchemaLoader, ICacheService cache, ILogger logger) - { - _ldapSchemaLoader = ldapSchemaLoader; - _cache = cache; - _logger = logger; - } - - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _logger.LogDebug("'{name}' started", nameof(LdapSchemaLoadingStep)); - ArgumentNullException.ThrowIfNull(context, nameof(context)); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration, nameof(context)); - - var schema = TryGetLdapSchema(context); - - if (schema is null) - { - _logger.LogWarning("Unable to load LDAP schema for '{domain}'", context.LdapServerConfiguration.ConnectionString); - throw new InvalidOperationException(); - } - - context.LdapSchema = schema; - return Task.CompletedTask; - } - - private ILdapSchema? TryGetLdapSchema(IRadiusPipelineExecutionContext context) - { - var cacheKey = context.LdapServerConfiguration!.ConnectionString; - if (_cache.TryGetValue(cacheKey, out ILdapSchema? schema)) - { - _logger.LogDebug("Loaded LDAP schema for '{domain}' from cache.", cacheKey); - return schema; - } - - var options = GetLdapConnectionOptions(context.LdapServerConfiguration); - schema = _ldapSchemaLoader.Load(options); - - if (schema is null) - return schema; - - var expirationDate = DateTimeOffset.Now.AddHours(context.LdapServerConfiguration.LdapSchemaCacheLifeTimeInHours); - SaveToCache(cacheKey, schema, expirationDate); - - _logger.LogDebug("LDAP schema for '{domain}' is saved in cache till '{expirationDate}'.", cacheKey, expirationDate.ToString()); - return schema; - } - - private LdapConnectionOptions GetLdapConnectionOptions(ILdapServerConfiguration serverConfiguration) - { - return new LdapConnectionOptions( - new LdapConnectionString(serverConfiguration.ConnectionString), - AuthType.Basic, - serverConfiguration.UserName, - serverConfiguration.Password, - TimeSpan.FromSeconds(serverConfiguration.BindTimeoutInSeconds)); - } - - private void SaveToCache(string cacheKey, ILdapSchema schema, DateTimeOffset expirationDate) - { - _cache.Set(cacheKey, schema, expirationDate); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthCheckStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthCheckStep.cs deleted file mode 100644 index a8cc9c57..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthCheckStep.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public class PreAuthCheckStep : IRadiusPipelineStep -{ - private readonly ILogger _logger; - - public PreAuthCheckStep(ILogger logger) - { - _logger = logger; - } - - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _logger.LogDebug("'{name}' started", nameof(PreAuthCheckStep)); - switch (context.PreAuthnMode.Mode) - { - case PreAuthMode.Otp when context.Passphrase.Otp == null: - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; - _logger.LogError("Pre-auth second factor was rejected: otp code is empty. User '{user:l}' from {host:l}:{port}", - context.RequestPacket.UserName, - context.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - context.ExecutionState.Terminate(); - return Task.CompletedTask; - - case PreAuthMode.None: - case PreAuthMode.Otp: - case PreAuthMode.Any: - _logger.LogDebug("Pre-auth check for '{user}' is completed.", context.RequestPacket.UserName); - return Task.CompletedTask; - - default: - throw new NotImplementedException($"Unknown pre-auth method: {context.PreAuthnMode.Mode}"); - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserGroupLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserGroupLoadingStep.cs deleted file mode 100644 index 92de9655..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserGroupLoadingStep.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.Ldap; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; - -public class UserGroupLoadingStep : IRadiusPipelineStep -{ - private readonly ILdapGroupService _ldapGroupService; - private readonly ILdapConnectionFactory _ldapConnectionFactory; - private readonly ILogger _logger; - - public UserGroupLoadingStep(ILdapGroupService groupService, ILdapConnectionFactory connectionFactory, ILogger logger) - { - _ldapGroupService = groupService; - _ldapConnectionFactory = connectionFactory; - _logger = logger; - } - - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) - { - _logger.LogDebug("'{name}' started", nameof(UserGroupLoadingStep)); - - if (ShouldSkipGroupLoading(context)) - return Task.CompletedTask; - - ArgumentNullException.ThrowIfNull(context.UserLdapProfile, nameof(context.UserLdapProfile)); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration, nameof(context.LdapServerConfiguration)); - - var userGroups = new HashSet(); - context.UserGroups = userGroups; - - foreach (var group in context.UserLdapProfile.MemberOf.Select(x => x.Components.Deepest.Value)) - userGroups.Add(group); - - if (!context.LdapServerConfiguration.LoadNestedGroups) - { - _logger.LogDebug("Nested groups for {domain} are not required.", context.LdapServerConfiguration.ConnectionString); - return Task.CompletedTask; - } - - LoadGroupsFromLdapCatalog(context, userGroups); - - return Task.CompletedTask; - } - - private void LoadGroupsFromLdapCatalog(IRadiusPipelineExecutionContext context, HashSet userGroups) - { - using var connection = _ldapConnectionFactory.CreateConnection(GetLdapConnectionOptions(context.LdapServerConfiguration!)); - - if (context.LdapServerConfiguration!.NestedGroupsBaseDns.Count > 0) - LoadUserGroupsFromContainers(context, userGroups, connection); - else - LoadUserGroupsFromRoot(context, userGroups, connection); - } - - private void LoadUserGroupsFromContainers(IRadiusPipelineExecutionContext context, HashSet userGroups, ILdapConnection connection) - { - foreach (var dn in context.LdapServerConfiguration!.NestedGroupsBaseDns) - { - _logger.LogDebug("Loading nested groups from '{dn}' at '{domain}' for '{user}'", dn, context.LdapServerConfiguration.ConnectionString, context.RequestPacket.UserName); - - var request = new LoadUserGroupsRequest( - context.LdapSchema!, - connection, - context.UserLdapProfile!.Dn, - dn); - - var groups = _ldapGroupService.LoadUserGroups(request); - var groupLog = string.Join("\n", groups); - _logger.LogDebug("Found groups at '{domain}' for '{user}': {groups}", dn, context.RequestPacket.UserName, groupLog); - - foreach (var group in groups) - userGroups.Add(group); - } - } - - private void LoadUserGroupsFromRoot(IRadiusPipelineExecutionContext context, HashSet userGroups, ILdapConnection connection) - { - var request = new LoadUserGroupsRequest( - context.LdapSchema!, - connection, - context.UserLdapProfile!.Dn); - - _logger.LogDebug("Loading nested groups from root at '{domain}' for '{user}'", context.LdapServerConfiguration!.ConnectionString, context.RequestPacket.UserName); - var groups = _ldapGroupService.LoadUserGroups(request); - - var groupLog = string.Join("\n", groups); - _logger.LogDebug("Found groups at root for '{user}': {groups}", context.RequestPacket.UserName, groupLog); - foreach (var group in groups) - userGroups.Add(group); - } - - private LdapConnectionOptions GetLdapConnectionOptions(ILdapServerConfiguration serverConfiguration) - { - return new LdapConnectionOptions( - new LdapConnectionString(serverConfiguration.ConnectionString), - AuthType.Basic, - serverConfiguration.UserName, - serverConfiguration.Password, - TimeSpan.FromSeconds(serverConfiguration.BindTimeoutInSeconds)); - } - - private bool ShouldSkipGroupLoading(IRadiusPipelineExecutionContext context) - { - return !AcceptedRequest(context) || GroupsNotRequired(context) || UnsupportedAccountType(context); - } - - private bool GroupsNotRequired(IRadiusPipelineExecutionContext context) - { - var notRequired = !context - .RadiusReplyAttributes - .Values - .SelectMany(x => x) - .Any(x => x.IsMemberOf || x.UserGroupCondition.Count > 0); - - if (!notRequired) - return false; - - _logger.LogDebug("User groups are not required."); - return true; - } - - private bool UnsupportedAccountType(IRadiusPipelineExecutionContext context) - { - if (context.IsDomainAccount) - return false; - - _logger.LogInformation( - "User '{user}' used '{accountType}' account to log in. User group loading is skipped.", - context.RequestPacket.UserName, - context.RequestPacket.AccountType); - - return true; - } - - private bool AcceptedRequest(IRadiusPipelineExecutionContext context) - { - return context.AuthenticationState.FirstFactorStatus is - AuthenticationStatus.Accept or AuthenticationStatus.Bypass - && context.AuthenticationState.SecondFactorStatus is - AuthenticationStatus.Accept or AuthenticationStatus.Bypass; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj b/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj index f5876cfc..d2b056ac 100644 --- a/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj +++ b/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj @@ -23,12 +23,12 @@ - + - + @@ -45,10 +45,6 @@ - - - - Always @@ -68,4 +64,9 @@ + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2/Program.cs b/src/Multifactor.Radius.Adapter.v2/Program.cs index 9a8b0180..0cc3e28d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Program.cs +++ b/src/Multifactor.Radius.Adapter.v2/Program.cs @@ -1,14 +1,12 @@ -using System.Reflection; -using System.Text; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Extensions; -using Multifactor.Radius.Adapter.v2.Server; -using Multifactor.Radius.Adapter.v2.Server.Udp; +using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Extensions; +using Multifactor.Radius.Adapter.v2.Application.Extensions; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; -using Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; +using Multifactor.Radius.Adapter.v2.Server; IHost? host = null; try @@ -16,33 +14,26 @@ var builder = Host.CreateApplicationBuilder(args); builder.Services.AddWindowsService(options => options.ServiceName = "Multifactor RADIUS"); builder.Services.AddMemoryCache(); - builder.Services.AddAdapterLogging(); - var appVars = new ApplicationVariables - { - AppPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), - AppVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(), - StartedAt = DateTime.Now - }; - - builder.Services.AddSingleton(appVars); - builder.Services.AddRadiusDictionary(); + builder.Services.AddApplicationVariables(); + builder.Services.AddConfiguration(); - - builder.Services.AddLdapSchemaLoader(); - builder.Services.AddDataProtectionService(); - + builder.Services.AddAdapterLogging(); + + builder.Services.AddLdap(); + + builder.Services.AddChallenge(); builder.Services.AddFirstFactor(); + builder.Services.AddPipelineSteps(); builder.Services.AddPipelines(); - - builder.Services.AddSingleton(); + builder.Services.AddTransient(); - - builder.Services.AddServices(); - builder.Services.AddChallenge(); - - builder.Services.AddUdpClient(); - builder.Services.AddMultifactorHttpClient(); - + + builder.Services.AddInfraServices(); + builder.Services.AddAppServices(); + + builder.Services.AddRadiusUdpClient(); + builder.Services.AddMultifactorApi(); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); host = builder.Build(); @@ -50,8 +41,13 @@ } catch (Exception ex) { - var errorMessage = FlattenException(ex); - StartupLogger.Error(ex, "Unable to start: {Message:l}", errorMessage); + // if(ex is InvalidConfigurationException) + // StartupLogger.Error(ex, "Unable to start: {Message:l}", ex.Message); + // else + // { + var errorMessage = FlattenException(ex); + StartupLogger.Error(ex, "Unable to start: {Message:l}", errorMessage); + // } } finally { diff --git a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs index bef98be4..85c48fc1 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -1,111 +1,190 @@ +using System.Collections.Concurrent; using System.Net.Sockets; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Server.Udp; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; namespace Multifactor.Radius.Adapter.v2.Server; -public class AdapterServer : IDisposable +internal sealed class AdapterServer : IAsyncDisposable { private readonly IUdpClient _udpClient; - private readonly IUdpPacketHandler _packetHandler; - private readonly ILogger _logger; - private readonly IServiceConfiguration _serviceConfiguration; + private readonly IRadiusUdpAdapter _packetAdapter; private readonly ApplicationVariables _applicationVariables; - private readonly IRadiusDictionary _radiusDictionary; + private readonly ILogger _logger; + private readonly ServiceConfiguration _serviceConfiguration; - private bool _isRunning; + private Task? _receiveLoopTask; + private CancellationTokenSource? _cts; + private readonly SemaphoreSlim _concurrencyLimiter; + private readonly ConcurrentBag _activeProcessingTasks = []; + + //TODO to the configuration + private const int ShoutDownTimeout = 30; + private const int MaxConcurrentRequests = 1000; public AdapterServer( IUdpClient udpClient, - IUdpPacketHandler handler, - IServiceConfiguration serviceConfiguration, + IRadiusUdpAdapter packetAdapter, ApplicationVariables applicationVariables, - IRadiusDictionary radiusDictionary, + ServiceConfiguration serviceConfiguration, ILogger logger) { _udpClient = udpClient; - _packetHandler = handler; - _serviceConfiguration = serviceConfiguration; + _packetAdapter = packetAdapter; _applicationVariables = applicationVariables; - _radiusDictionary = radiusDictionary; + _serviceConfiguration = serviceConfiguration; _logger = logger; + + _concurrencyLimiter = new SemaphoreSlim(MaxConcurrentRequests); } - public async Task StartAsync(CancellationToken cancellationToken) - { - if (_isRunning) - { - _logger.LogInformation("Server is already running."); - return; - } - - _isRunning = true; - LogHelloMessage(); - UdpReceiveResult udpPacket = new UdpReceiveResult(); - while (_isRunning && !cancellationToken.IsCancellationRequested) - { - try - { - var packet = await ReceivePackets(); - udpPacket = packet; - _logger.LogInformation("Received packet from {host:l}:{port}.", packet.RemoteEndPoint.Address, packet.RemoteEndPoint.Port); - var task = Task.Factory.StartNew(() => ProcessPacket(packet), TaskCreationOptions.LongRunning); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error while processing packet from '{client:l}'", udpPacket.RemoteEndPoint.Address); - } - } - } - - public Task Stop() + public Task StartAsync(CancellationToken cancellationToken = default) { - if (!_isRunning) + if (_receiveLoopTask != null) { + _logger.LogWarning("Server is already running"); return Task.CompletedTask; } + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + LogStartupMessage(); - _logger.LogInformation("Stopping server"); - _isRunning = false; - _udpClient?.Dispose(); - _logger.LogInformation("Server is stopped"); + try + { + _receiveLoopTask = Task.Run(() => ReceiveLoopAsync(_cts.Token), _cts.Token); + _logger.LogInformation("RADIUS server started successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to start RADIUS server"); + throw; + } + return Task.CompletedTask; } - private void LogHelloMessage() + private async Task ReceiveLoopAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Multifactor (c) cross-platform RADIUS Adapter, v. {Version:l}", _applicationVariables.AppVersion); - _logger.LogInformation("Starting Radius server on {host:l}:{port}", - _serviceConfiguration.ServiceServerEndpoint.Address, - _serviceConfiguration.ServiceServerEndpoint.Port); - - _logger.LogInformation(_radiusDictionary.GetInfo()); + _logger.LogDebug("Starting UDP receive loop"); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await _concurrencyLimiter.WaitAsync(cancellationToken); + + var packet = await _udpClient.ReceiveAsync(cancellationToken); + + var processingTask = ProcessPacketAsync(packet, cancellationToken); + + _activeProcessingTasks.Add(processingTask); + + _ = processingTask.ContinueWith(t => + { + _activeProcessingTasks.TryTake(out _); + _concurrencyLimiter.Release(); + }, TaskScheduler.Default); + + _logger.LogDebug("Received packet from {Host}:{Port}", + packet.RemoteEndPoint.Address, packet.RemoteEndPoint.Port); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in UDP receive loop"); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + } + + _logger.LogDebug("UDP receive loop stopped"); } - private async Task ProcessPacket(UdpReceiveResult udpPacket) + private async Task ProcessPacketAsync(UdpReceiveResult udpPacket, CancellationToken cancellationToken) { try { - await _packetHandler.HandleUdpPacket(udpPacket); + await _packetAdapter.Handle(udpPacket); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { } catch (Exception ex) { - _logger.LogError(ex, "Failed to process packet from {host:l}:{port}", udpPacket.RemoteEndPoint.Address, udpPacket.RemoteEndPoint.Port); + _logger.LogError(ex, + "Failed to process packet from {Host}:{Port}", + udpPacket.RemoteEndPoint.Address, + udpPacket.RemoteEndPoint.Port); } + } + + private async Task StopAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Stopping RADIUS server..."); + + if(_cts != null) + await _cts.CancelAsync(); - await Task.CompletedTask; + if (_receiveLoopTask != null) + { + try + { + await _receiveLoopTask.WaitAsync( + TimeSpan.FromSeconds(ShoutDownTimeout), + cancellationToken); + } + catch (TimeoutException) + { + _logger.LogWarning("Receive loop did not stop gracefully within timeout"); + } + catch (OperationCanceledException) + { + // shoutdown was canceled + } + } + + if (!_activeProcessingTasks.IsEmpty) + { + _logger.LogDebug("Waiting for {Count} active processing tasks to complete", + _activeProcessingTasks.Count); + + try + { + await Task.WhenAll(_activeProcessingTasks) + .WaitAsync(TimeSpan.FromSeconds(ShoutDownTimeout), cancellationToken); + } + catch (TimeoutException) + { + _logger.LogWarning("Some processing tasks did not complete within timeout"); + } + } + + _logger.LogInformation("RADIUS server stopped"); } - private Task ReceivePackets() + private void LogStartupMessage() { - return _udpClient.ReceiveAsync(); + _logger.LogInformation("Multifactor (c) cross-platform RADIUS Adapter, v. {Version:l}", _applicationVariables.AppVersion); + var endpoint = _serviceConfiguration.RootConfiguration.AdapterServerEndpoint; + _logger.LogInformation( + "Starting RADIUS server on {Host}:{Port} (Max concurrent: {MaxConcurrent})", + endpoint.Address, + endpoint.Port, + MaxConcurrentRequests); } - public void Dispose() + public async ValueTask DisposeAsync() { - Stop(); + await StopAsync(); + + _concurrencyLimiter.Dispose(); + _cts?.Dispose(); + _udpClient.Dispose(); + + GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/Multifactor.Radius.Adapter.v2/Server/IRadiusPacketProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Server/IRadiusPacketProcessor.cs deleted file mode 100644 index 5f01ca1e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Server/IRadiusPacketProcessor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; - -namespace Multifactor.Radius.Adapter.v2.Server; - -public interface IRadiusPacketProcessor -{ - Task ProcessPacketAsync(IRadiusPacket requestPacket, IClientConfiguration clientConfiguration); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/RadiusPacketProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Server/RadiusPacketProcessor.cs deleted file mode 100644 index 6046c445..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Server/RadiusPacketProcessor.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Pipeline.Settings; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; -using Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -namespace Multifactor.Radius.Adapter.v2.Server; - -public class RadiusPacketProcessor : IRadiusPacketProcessor -{ - private readonly IPipelineProvider _pipelineProvider; - private readonly IResponseSender _responseSender; - private readonly ILdapServerConfigurationService _ldapServerConfigurationService; - private readonly ILdapForestService _ldapForestService; - private readonly ILogger _logger; - - public RadiusPacketProcessor( - IPipelineProvider pipelineProvider, - IResponseSender responseSender, - ILdapServerConfigurationService ldapServerConfigurationService, - ILdapForestService ldapForestService, - ILogger logger) - { - _pipelineProvider = pipelineProvider; - _responseSender = responseSender; - _ldapServerConfigurationService = ldapServerConfigurationService; - _ldapForestService = ldapForestService; - _logger = logger; - } - - public async Task ProcessPacketAsync(IRadiusPacket requestPacket, IClientConfiguration clientConfiguration) - { - _logger.LogDebug("Start processing '{type}' packet.", requestPacket.Code); - if (clientConfiguration.LdapServers.Count <= 0 || requestPacket.Code != PacketCode.AccessRequest) - { - await ExecutePipeline(clientConfiguration, requestPacket); - return; - } - - foreach (var serverConfig in clientConfiguration.LdapServers) - { - var forest = _ldapForestService.LoadLdapForest( - Utils.CreateLdapConnectionOptions(serverConfig), - serverConfig.TrustedDomainsEnabled, - serverConfig.AlternativeSuffixesEnabled); - - if (!forest.Any()) - { - _logger.LogWarning("Failed to load LDAP forest for '{domain}'", serverConfig.ConnectionString); - continue; - } - - var filteredForest = ApplyPermissions(forest, serverConfig.DomainPermissions, serverConfig.SuffixesPermissions); - - var configs = GetLdapServerConfigurations(filteredForest, serverConfig); - - foreach (var config in configs) - { - var isSuccessful = await ExecutePipeline(clientConfiguration, requestPacket, config); - if (isSuccessful) - return; - } - } - } - - private async Task ExecutePipeline(IClientConfiguration clientConfiguration, IRadiusPacket requestPacket, ILdapServerConfiguration? ldapServerConfiguration = null) - { - var context = CreatePipelineContext(clientConfiguration, requestPacket, ldapServerConfiguration); - var pipeline = GetPipeline(clientConfiguration.Name); - var logMessage = $"Start executing pipeline for '{clientConfiguration.Name}'" + (ldapServerConfiguration is not null ? $" at '{ldapServerConfiguration.ConnectionString}'" : string.Empty); - _logger.LogDebug(logMessage); - - try - { - await pipeline.ExecuteAsync(context); - var responseRequest = GetResponseRequest(context); - await _responseSender.SendResponse(responseRequest); - return true; - } - catch (Exception e) - { - var errMessage = $"Failed to execute pipeline for '{clientConfiguration.Name}'" + (ldapServerConfiguration is not null ? $" at '{ldapServerConfiguration.ConnectionString}'" : string.Empty); - _logger.LogWarning(exception: e, errMessage); - } - - return false; - } - - private RadiusPipelineExecutionContext CreatePipelineContext(IClientConfiguration clientConfiguration, IRadiusPacket requestPacket, ILdapServerConfiguration? ldapServerConfiguration = null) - { - var executionSetting = new PipelineExecutionSettings(clientConfiguration, ldapServerConfiguration); - var context = new RadiusPipelineExecutionContext(executionSetting, requestPacket) - { - Passphrase = UserPassphrase.Parse(requestPacket.TryGetUserPassword(), clientConfiguration.PreAuthnMode) - }; - return context; - } - - private IRadiusPipeline GetPipeline(string clientConfigurationName) - { - var pipeline = _pipelineProvider.GetRadiusPipeline(clientConfigurationName); - if (pipeline is null) - throw new Exception($"No pipeline found for client {clientConfigurationName}, check adapter configuration and restart the adapter."); - return pipeline; - } - - private SendAdapterResponseRequest GetResponseRequest(IRadiusPipelineExecutionContext context) => new(context); - - private IEnumerable ApplyPermissions(IEnumerable forest, IPermissionRules domainPermissions, IPermissionRules suffixesPermissions) - { - var filter = new ForestFilter(); - var filtered = filter.FilterDomains(forest, domainPermissions); - filtered = filter.FilterSuffixes(filtered, suffixesPermissions); - return filtered; - } - - private IEnumerable GetLdapServerConfigurations(IEnumerable forest, ILdapServerConfiguration serverConfig) - { - return _ldapServerConfigurationService.DuplicateConfigurationForDn(forest.Select(x => x.Schema.NamingContext), serverConfig); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs index 2254629e..f3651079 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs @@ -1,47 +1,64 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; namespace Multifactor.Radius.Adapter.v2.Server; -public class ServerHost : IHostedService +internal sealed class ServerHost : IHostedService { private readonly AdapterServer _server; private readonly ILogger _logger; + private Task? _serverTask; + private CancellationTokenSource? _cts; + private const int ShoutDownTimeout = 30; public ServerHost(AdapterServer server, ILogger logger) { - Throw.IfNull(server, nameof(server)); - Throw.IfNull(logger, nameof(logger)); - _server = server; - _logger = logger; + _server = server ?? throw new ArgumentNullException(nameof(server)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken = default) { + _logger.LogInformation("Starting RADIUS server host..."); + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + try { - var task = _server.StartAsync(cancellationToken); + _serverTask = _server.StartAsync(_cts.Token); + await Task.Yield(); + _logger.LogInformation("RADIUS server host started"); } catch (Exception ex) { - _logger.LogError(ex, ex.Message); + _logger.LogError(ex, "Failed to start RADIUS server host"); + throw; } - - return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { + _logger.LogInformation("Stopping RADIUS server host..."); try { - _server.Stop(); + if(_cts != null) + await _cts.CancelAsync(); + + if (_serverTask is { IsCompleted: false }) + { + await Task.WhenAny(_serverTask, + Task.Delay(TimeSpan.FromSeconds(ShoutDownTimeout), cancellationToken)); + } + + _logger.LogInformation("RADIUS server host stopped"); } catch (Exception ex) { - _logger.LogError(ex, ex.Message); + _logger.LogError(ex, "Error during RADIUS server host shutdown"); + throw; + } + finally + { + _cts?.Dispose(); } - - return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2/Server/Udp/CustomUdpClient.cs deleted file mode 100644 index d55fe4ab..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Server/Udp/CustomUdpClient.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Multifactor.Core.Ldap.LangFeatures; - -namespace Multifactor.Radius.Adapter.v2.Server.Udp; - -public sealed class CustomUdpClient : IUdpClient -{ - private readonly UdpClient _udpClient; - - public CustomUdpClient(IPEndPoint endPoint) - { - Throw.IfNull(endPoint, nameof(endPoint)); - _udpClient = new UdpClient(endPoint); - } - - public CustomUdpClient(string endPoint) - { - Throw.IfNullOrWhiteSpace(endPoint, nameof(endPoint)); - _udpClient = new UdpClient(IPEndPoint.Parse(endPoint)); - } - - public Task ReceiveAsync() => _udpClient.ReceiveAsync(); - - public Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint) => _udpClient.SendAsync(datagram, bytesCount, endPoint); - - public void Dispose() - { - _udpClient?.Close(); - _udpClient?.Dispose(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpPacketHandler.cs b/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpPacketHandler.cs deleted file mode 100644 index f879e102..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpPacketHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Net.Sockets; - -namespace Multifactor.Radius.Adapter.v2.Server.Udp; - -public interface IUdpPacketHandler -{ - Task HandleUdpPacket(UdpReceiveResult udpPacket); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/AdapterResponseSender.cs deleted file mode 100644 index 67ca84b8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/AdapterResponseSender.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Server.Udp; -using Multifactor.Radius.Adapter.v2.Services.Radius; - -namespace Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; - -public class AdapterResponseSender : IResponseSender -{ - private readonly IRadiusPacketService _radiusPacketService; - private readonly IRadiusReplyAttributeService _radiusReplyAttributeService; - private readonly IUdpClient _udpClient; - private readonly ILogger _logger; - public AdapterResponseSender( - IRadiusPacketService radiusPacketService, - IUdpClient udpClient, - IRadiusReplyAttributeService radiusReplyAttributeService, - ILogger logger) - { - Throw.IfNull(radiusPacketService, nameof(radiusPacketService)); - Throw.IfNull(udpClient, nameof(udpClient)); - - _radiusPacketService = radiusPacketService; - _radiusReplyAttributeService = radiusReplyAttributeService; - _udpClient = udpClient; - _logger = logger; - } - - public async Task SendResponse(SendAdapterResponseRequest request) - { - if (request.ShouldSkipResponse) - return; - - if (request.ResponsePacket?.IsEapMessageChallenge == true) - { - //EAP authentication in process, just proxy response - _logger.LogDebug("Proxying EAP-Message Challenge to {host:l}:{port} id={id}", request.RemoteEndpoint.Address, request.RemoteEndpoint.Port, request.RequestPacket.Identifier); - await SendResponse(request.ResponsePacket, request); - return; - } - - if (request.RequestPacket.IsVendorAclRequest && request.ResponsePacket != null) - { - //ACL and other rules transfer, just proxy response - _logger.LogDebug("Proxying #ACSACL# to {host:l}:{port} id={id}", request.RemoteEndpoint.Address, request.RemoteEndpoint.Port, request.RequestPacket.Identifier); - await SendResponse(request.ResponsePacket, request); - return; - } - - var responsePacket = BuildResponsePacket(request); - - await SendResponse(responsePacket, request); - var endpoint = request.ProxyEndpoint ?? request.RemoteEndpoint; - - if (!string.IsNullOrWhiteSpace(request.RequestPacket.UserName)) - _logger.LogInformation("{code:l} sent to {host:l}:{port} id={id} user='{user:l}'", responsePacket.Code.ToString(), endpoint.Address, endpoint.Port, responsePacket.Identifier, request.RequestPacket.UserName); - else - _logger.LogInformation("{code:l} sent to {host:l}:{port} id={id}", responsePacket.Code.ToString(), endpoint.Address, endpoint.Port, responsePacket.Identifier); - } - - private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) - { - var requestPacket = request.RequestPacket; - var responsePacketCode = ToPacketCode(request.AuthenticationState); - var responsePacket = _radiusPacketService.CreateResponsePacket(requestPacket, responsePacketCode); - - switch (responsePacketCode) - { - case PacketCode.AccessAccept: - AddResponsePacketAttributes(request.ResponsePacket, responsePacket); - AddReplyAttributes(responsePacket, request); - break; - case PacketCode.AccessReject: - if (request.ResponsePacket != null && request.ResponsePacket.Code == PacketCode.AccessReject) - AddResponsePacketAttributes(request.ResponsePacket, responsePacket); - break; - case PacketCode.AccessChallenge: - if (!string.IsNullOrWhiteSpace(request.ResponseInformation.State)) - responsePacket.ReplaceAttribute("State", request.ResponseInformation.State); - break; - default: - throw new NotImplementedException(responsePacketCode.ToString()); - } - - if (!string.IsNullOrWhiteSpace(request.ResponseInformation.ReplyMessage)) - responsePacket.ReplaceAttribute("Reply-Message", request.ResponseInformation.ReplyMessage); - - AddProxyAttribute(requestPacket, responsePacket); - - AddMessageAuthenticator(responsePacket); - - return responsePacket; - } - - private void AddResponsePacketAttributes(IRadiusPacket? source, RadiusPacket target) - { - if (source is null) - return; - foreach (var attribute in source.Attributes.Values) - { - target.RemoveAttribute(attribute.Name); - foreach (var value in attribute.Values) - target.AddAttributeValue(attribute.Name, value); - } - } - - private void AddProxyAttribute(IRadiusPacket source, RadiusPacket target) - { - if (!source.Attributes.ContainsKey("Proxy-State")) - return; - if (!target.Attributes.ContainsKey("Proxy-State")) - target.ReplaceAttribute("Proxy-State", source.Attributes.SingleOrDefault(o => o.Key == "Proxy-State").Value.Values.Single()); - } - - private void AddMessageAuthenticator(RadiusPacket target) - { - if (target.Attributes.ContainsKey("Message-Authenticator")) - return; - - var placeholder = new byte[16]; - var placeholderStr = Encoding.Default.GetString(placeholder); - target.AddAttributeValue("Message-Authenticator", placeholderStr); - } - - private void AddReplyAttributes(RadiusPacket target, SendAdapterResponseRequest request) - { - var replyAttributesRequest = new GetReplyAttributesRequest( - request.RequestPacket.UserName, - request.UserGroups, - request.RadiusReplyAttributes, - request.Attributes); - - var attributes = _radiusReplyAttributeService.GetReplyAttributes(replyAttributesRequest); - foreach (var attribute in attributes) - { - target.RemoveAttribute(attribute.Key); - foreach (var attrValue in attribute.Value) - target.AddAttributeValue(attribute.Key, attrValue); - } - } - - private PacketCode ToPacketCode(IAuthenticationState authenticationState) - { - var successfulFirstFactor = authenticationState.FirstFactorStatus is AuthenticationStatus.Accept or AuthenticationStatus.Bypass; - var successfulSecondFactor = authenticationState.SecondFactorStatus is AuthenticationStatus.Accept or AuthenticationStatus.Bypass; - if (successfulFirstFactor && successfulSecondFactor) - return PacketCode.AccessAccept; - var authFailed = authenticationState.FirstFactorStatus == AuthenticationStatus.Reject || authenticationState.SecondFactorStatus == AuthenticationStatus.Reject; - return authFailed ? PacketCode.AccessReject : PacketCode.AccessChallenge; - } - - private async Task SendResponse(IRadiusPacket responsePacket, SendAdapterResponseRequest request) - { - var bytes = _radiusPacketService.GetBytes(responsePacket, request.RadiusSharedSecret); - var endpoint = request.ProxyEndpoint ?? request.RemoteEndpoint; - if (responsePacket.Code == PacketCode.AccessReject) - await new RandomWaiter(request.InvalidCredentialDelay).WaitSomeTimeAsync(); - await _udpClient.SendAsync(bytes, bytes.Length, endpoint); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/SendAdapterResponseRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/SendAdapterResponseRequest.cs deleted file mode 100644 index c078c390..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/SendAdapterResponseRequest.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Net; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Services.AdapterResponseSender; - -public class SendAdapterResponseRequest -{ - public bool ShouldSkipResponse { get; } - public IRadiusPacket? ResponsePacket { get; } - public IRadiusPacket RequestPacket { get; } - public IPEndPoint RemoteEndpoint { get; } - public IPEndPoint? ProxyEndpoint { get; } - public IAuthenticationState AuthenticationState { get; } - public IResponseInformation ResponseInformation { get; } - public SharedSecret RadiusSharedSecret { get; } - public HashSet UserGroups { get; } - public IReadOnlyDictionary RadiusReplyAttributes { get; } - public IReadOnlyCollection Attributes { get; } - public RandomWaiterConfig InvalidCredentialDelay { get; } - - public SendAdapterResponseRequest(IRadiusPipelineExecutionContext context) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.RequestPacket); - ArgumentNullException.ThrowIfNull(context.RemoteEndpoint); - ArgumentNullException.ThrowIfNull(context.AuthenticationState); - ArgumentNullException.ThrowIfNull(context.ResponseInformation); - ArgumentNullException.ThrowIfNull(context.RadiusSharedSecret); - ArgumentNullException.ThrowIfNull(context.UserGroups); - ArgumentNullException.ThrowIfNull(context.RadiusReplyAttributes); - - ShouldSkipResponse = context.ExecutionState.ShouldSkipResponse; - ResponsePacket = context.ResponsePacket; - RequestPacket = context.RequestPacket; - RemoteEndpoint = context.RemoteEndpoint; - ProxyEndpoint = context.ProxyEndpoint; - AuthenticationState = context.AuthenticationState; - ResponseInformation = context.ResponseInformation; - RadiusSharedSecret = context.RadiusSharedSecret; - UserGroups = context.UserGroups; - RadiusReplyAttributes = context.RadiusReplyAttributes; - Attributes = context.UserLdapProfile?.Attributes ?? Array.Empty(); - InvalidCredentialDelay = context.InvalidCredentialDelay; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs deleted file mode 100644 index 57528c96..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; - -namespace Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; - - public class AuthenticatedClientCache : IAuthenticatedClientCache - { - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary _authenticatedClients = new(); - private readonly ILogger _logger; - - public AuthenticatedClientCache(ILogger logger) - { - _logger = logger; - } - - public bool TryHitCache(string? callingStationId, string userName, string clientName, AuthenticatedClientCacheConfig cacheConfig) - { - ArgumentNullException.ThrowIfNull(cacheConfig); - ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - - if (!cacheConfig.Enabled) - return false; - - if (string.IsNullOrWhiteSpace(callingStationId)) - { - _logger.LogError("Remote host parameter miss for user {userName:l}", userName); - return false; - } - - var id = AuthenticatedClient.ParseId(callingStationId, clientName, userName); - if (!_authenticatedClients.TryGetValue(id, out var authenticatedClient)) - return false; - - _logger.LogDebug($"User {userName} with calling-station-id {callingStationId} authenticated {authenticatedClient.Elapsed:hh\\:mm\\:ss} ago. Authentication session period: {cacheConfig.Lifetime}"); - - if (authenticatedClient.Elapsed <= cacheConfig.Lifetime) - return true; - - _authenticatedClients.TryRemove(id, out _); - - return false; - } - - public void SetCache(string? callingStationId, string? userName, string clientName, AuthenticatedClientCacheConfig cacheConfig) - { - ArgumentNullException.ThrowIfNull(cacheConfig); - ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - - if (!cacheConfig.Enabled || string.IsNullOrWhiteSpace(callingStationId)) - return; - - var client = AuthenticatedClient.Create(callingStationId, clientName, userName); - var added = false; - if (!_authenticatedClients.ContainsKey(client.Id)) - added = _authenticatedClients.TryAdd(client.Id, client); - - if (added) - { - var expirationDate = DateTimeOffset.Now.Add(cacheConfig.Lifetime); - _logger.LogDebug("Authentication for user '{userName}' is saved in cache till '{expiration}' with key '{key}'", userName, expirationDate.ToString(), client.Id); - } - else - _logger.LogWarning("Failed to save user '{userName}' with key '{key}' to cache", userName, client.Id); - } - } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/IAuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/IAuthenticatedClientCache.cs deleted file mode 100644 index 66bbbd5a..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/IAuthenticatedClientCache.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Auth; - -namespace Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; - -public interface IAuthenticatedClientCache -{ - void SetCache(string? callingStationId, string userName, string clientName, AuthenticatedClientCacheConfig clientConfiguration); - bool TryHitCache(string? callingStationId, string userName, string clientName, AuthenticatedClientCacheConfig cacheConfig); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/IDataProtectionService.cs b/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/IDataProtectionService.cs deleted file mode 100644 index 19f642d1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/IDataProtectionService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.DataProtection; - -public interface IDataProtectionService -{ - string Protect(string secret, string data); - - string Unprotect(string secret, string data); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/LinuxProtectionService.cs b/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/LinuxProtectionService.cs deleted file mode 100644 index 2abc376f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/LinuxProtectionService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.Services.DataProtection; - -public class LinuxProtectionService : IDataProtectionService -{ - public string Protect(string secret, string data) - { - ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); - - byte[] bytes = StringToBytes(data); - return ToBase64(bytes); - } - - public string Unprotect(string secret, string data) - { - ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); - - byte[] bytes = FromBase64(data); - return BytesToString(bytes); - } - - private static byte[] StringToBytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } - - private static string BytesToString(byte[] b) - { - return Encoding.UTF8.GetString(b); - } - - private static string ToBase64(byte[] data) - { - return Convert.ToBase64String(data); - } - - private static byte[] FromBase64(string text) - { - return Convert.FromBase64String(text); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/WindowsProtectionService.cs b/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/WindowsProtectionService.cs deleted file mode 100644 index 9f15e4f0..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/DataProtection/WindowsProtectionService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.Services.DataProtection; - -public class WindowsProtectionService : IDataProtectionService -{ - public string Protect(string secret, string data) - { - ArgumentException.ThrowIfNullOrWhiteSpace(secret, nameof(secret)); - ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); - - var additionalEntropy = StringToBytes(secret); - return ToBase64(ProtectedData.Protect(StringToBytes(data), additionalEntropy, DataProtectionScope.CurrentUser)); - } - - public string Unprotect(string secret, string data) - { - ArgumentException.ThrowIfNullOrWhiteSpace(secret, nameof(secret)); - ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); - - var additionalEntropy = StringToBytes(secret); - return BytesToString(ProtectedData.Unprotect(FromBase64(data), additionalEntropy, DataProtectionScope.CurrentUser)); - } - - private byte[] StringToBytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } - - private string BytesToString(byte[] b) - { - return Encoding.UTF8.GetString(b); - } - - private string ToBase64(byte[] data) - { - return Convert.ToBase64String(data); - } - - private byte[] FromBase64(string text) - { - return Convert.FromBase64String(text); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ChangeUserPasswordRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ChangeUserPasswordRequest.cs deleted file mode 100644 index a8d29765..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ChangeUserPasswordRequest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class ChangeUserPasswordRequest -{ - public string NewPassword { get; } - public ILdapProfile Profile { get; } - public ILdapServerConfiguration ServerConfiguration { get; } - public ILdapSchema Schema { get; } - - public ChangeUserPasswordRequest(string newPassword, ILdapProfile profile, ILdapServerConfiguration configuration, ILdapSchema schema) - { - ArgumentException.ThrowIfNullOrWhiteSpace(newPassword); - ArgumentNullException.ThrowIfNull(profile); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(schema); - - NewPassword = newPassword; - Profile = profile; - ServerConfiguration = configuration; - Schema = schema; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/CustomLdapSchemaLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/CustomLdapSchemaLoader.cs deleted file mode 100644 index ea5ebf28..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/CustomLdapSchemaLoader.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class CustomLdapSchemaLoader : ILdapSchemaLoader -{ - private readonly ILdapSchemeLoaderWrapper _ldapSchemaLoader; - private readonly ILogger _logger; - - public CustomLdapSchemaLoader( - ILdapSchemeLoaderWrapper ldapSchemaLoader, - ILogger logger) - { - ArgumentNullException.ThrowIfNull(ldapSchemaLoader, nameof(ldapSchemaLoader)); - ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - - _ldapSchemaLoader = ldapSchemaLoader; - _logger = logger; - } - - public ILdapSchema? Load(LdapConnectionOptions connectionOptions) - { - ILdapSchema? schema = null; - try - { - schema = _ldapSchemaLoader.Load(connectionOptions); - } - catch (Exception e) - { - _logger.LogError(e, "Error during loading LDAP schema."); - } - - if (schema is null) - { - _logger.LogWarning("Failed to load LDAP schema of '{url}'", connectionOptions.ConnectionString.Host); - return schema; - } - - _logger.LogDebug("Successfully loaded LDAP schema of '{url}'", connectionOptions.ConnectionString.Host); - return schema; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/FindUserProfileRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/FindUserProfileRequest.cs deleted file mode 100644 index f9ccef05..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/FindUserProfileRequest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class FindUserProfileRequest -{ - public string ClientName { get; } - public ILdapServerConfiguration LdapServerConfiguration { get; } - public ILdapSchema LdapSchema { get; } - public DistinguishedName SearchBase { get; } - public UserIdentity UserIdentity { get; } - public LdapAttributeName[]? AttributeNames { get; } - - public FindUserProfileRequest(string clientName, ILdapServerConfiguration configuration, ILdapSchema ldapSchema, DistinguishedName searchBase, UserIdentity userIdentity, LdapAttributeName[]? attributeNames = null) - { - ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(ldapSchema); - ArgumentNullException.ThrowIfNull(searchBase); - ArgumentNullException.ThrowIfNull(userIdentity); - - ClientName = clientName; - LdapServerConfiguration = configuration; - LdapSchema = ldapSchema; - SearchBase = searchBase; - UserIdentity = userIdentity; - AttributeNames = attributeNames; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ActiveDirectoryLdapForestLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ActiveDirectoryLdapForestLoader.cs deleted file mode 100644 index 8b6331c9..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ActiveDirectoryLdapForestLoader.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap.Entry; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public class ActiveDirectoryLdapForestLoader : ILdapForestLoader -{ - private readonly string _domainLocation = "cn=System"; - private readonly string _domainObjectClass = "trustedDomain"; - private readonly string _suffixLocation = "cn=Partitions,cn=Configuration"; - private readonly string _suffixAttribute = "uPNSuffixes"; - - public LdapImplementation LdapImplementation => LdapImplementation.ActiveDirectory; - - public IEnumerable LoadTrustedDomains(ILdapConnection connection, ILdapSchema schema) - { - var trustedDomainsResult = connection.Find( - new DistinguishedName($"{_domainLocation},{schema.NamingContext.StringRepresentation}"), - $"{schema.ObjectClass}={_domainObjectClass}", - SearchScope.OneLevel, - attributes: schema.Cn); - - var trustedDomains = trustedDomainsResult - .Select(x => GetAttributeValue(x, schema.Cn)) - .Where(x => x.Count != 0) - .SelectMany(x => x) - .Select(LdapNamesUtils.FqdnToDn); - - return trustedDomains; - } - - public IEnumerable LoadDomainSuffixes(ILdapConnection connection, ILdapSchema schema) - { - var upnSuffixesResult = connection.Find( - new DistinguishedName($"{_suffixLocation},{schema.NamingContext.StringRepresentation}"), - $"{schema.ObjectClass}=*", - SearchScope.Base, - attributes: _suffixAttribute); - - var upnSuffixes = upnSuffixesResult - .Select(x => GetAttributeValue(x, _suffixAttribute)) - .Where(x => x.Count != 0) - .SelectMany(x => x); - - return upnSuffixes; - } - - private IReadOnlyCollection GetAttributeValue(LdapEntry entry, string attributeName) - { - var attribute = entry.Attributes[attributeName]; - return attribute is null ? [] : attribute.GetNotEmptyValues(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoader.cs deleted file mode 100644 index 993bb61d..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoader.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public interface ILdapForestLoader -{ - public LdapImplementation LdapImplementation { get; } - - public IEnumerable LoadTrustedDomains(ILdapConnection connection, ILdapSchema schema); - - public IEnumerable LoadDomainSuffixes(ILdapConnection connection, ILdapSchema schema); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoaderProvider.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoaderProvider.cs deleted file mode 100644 index 28c69981..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoaderProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public interface ILdapForestLoaderProvider -{ - public ILdapForestLoader? GetTrustedDomainsLoader(LdapImplementation ldapImplementation); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestService.cs deleted file mode 100644 index c97a1b78..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Core.Ldap.Connection; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public interface ILdapForestService -{ - IReadOnlyCollection LoadLdapForest(LdapConnectionOptions connectionOptions, bool loadTrustedDomains, bool loadSuffixes); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapServerConfigurationService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapServerConfigurationService.cs deleted file mode 100644 index 118fc6a4..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapServerConfigurationService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public interface ILdapServerConfigurationService -{ - IEnumerable DuplicateConfigurationForDn(IEnumerable targetDomains, ILdapServerConfiguration initialConfiguration); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestLoaderProvider.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestLoaderProvider.cs deleted file mode 100644 index d52ed81b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestLoaderProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public class LdapForestLoaderProvider: ILdapForestLoaderProvider -{ - private readonly IEnumerable _trustedDomainsLoaders; - - public LdapForestLoaderProvider(IEnumerable loaders) - { - _trustedDomainsLoaders = loaders; - } - - public ILdapForestLoader? GetTrustedDomainsLoader(LdapImplementation ldapImplementation) - { - return _trustedDomainsLoaders.FirstOrDefault(x => x.LdapImplementation == ldapImplementation); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestService.cs deleted file mode 100644 index 1dd6d19e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Forest; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public class LdapForestService : ILdapForestService -{ - private readonly ILdapSchemaLoader _ldapSchemaLoader; - private readonly ILogger _logger; - private readonly ILdapConnectionFactory _connectionFactory; - private readonly ILdapForestLoaderProvider _ldapForestLoaderProvider; - private readonly ICacheService _cache; - - public LdapForestService( - ILdapSchemaLoader ldapSchemaLoader, - ILdapConnectionFactory connectionFactory, - ILdapForestLoaderProvider ldapForestLoaderProvider, - ICacheService cache, - ILogger logger) - { - _ldapSchemaLoader = ldapSchemaLoader; - _connectionFactory = connectionFactory; - _ldapForestLoaderProvider = ldapForestLoaderProvider; - _cache = cache; - _logger = logger; - } - - /// - /// Loads root and trusted domains - /// - public IReadOnlyCollection LoadLdapForest(LdapConnectionOptions connectionOptions, bool loadTrustedDomains, bool loadSuffixes) - { - var domain = connectionOptions.ConnectionString.Host; - var cacheKey = BuildCacheKey(domain); - var forest = TryGetForestFromCache(cacheKey); - if (forest != null) - { - _logger.LogDebug("Loaded LDAP forest for '{domain}' from cache.", domain); - return forest; - } - - forest = LoadForest(connectionOptions, loadTrustedDomains, loadSuffixes); - var expirationDate = DateTimeOffset.Now.AddHours(1); - _cache.Set(cacheKey, forest, expirationDate); - - return forest; - } - - private IReadOnlyCollection LoadForest(LdapConnectionOptions connectionOptions, bool loadTrustedDomains, bool loadSuffixes) - { - var domain = connectionOptions.ConnectionString.Host; - var mainSchema = LoadSchema(connectionOptions); - - if (mainSchema is null) - return Array.Empty(); - - var loader = GetForestLoader(mainSchema.LdapServerImplementation); - - if (loader is null) - { - _logger.LogDebug("Adapter does not support trusted domains feature for '{catalogType}' catalog '{domain}'. Loading is skipped", mainSchema.LdapServerImplementation, domain); - return new List() { new(mainSchema, [LdapNamesUtils.DnToFqdn(mainSchema.NamingContext)])}; - } - - _logger.LogDebug("Loading forest schema from '{domain}'", domain); - using var connection = _connectionFactory.CreateConnection(connectionOptions); - - var schemas = new List { mainSchema }; - if (loadTrustedDomains) - schemas.AddRange(LoadTrustedSchemas(connection, loader, connectionOptions, mainSchema)); - else - _logger.LogDebug("Trusted domains are not required for '{domain}'", domain); - - var forest = new List(); - - foreach (var schema in schemas) - { - var fqdn = LdapNamesUtils.DnToFqdn(schema.NamingContext); - var forestEntry = new LdapForestEntry(schema, [fqdn]); - forest.Add(forestEntry); - if (!loadSuffixes) - continue; - - var suffixes = loader.LoadDomainSuffixes(connection, schema).ToList(); - - if (suffixes.Any()) - { - var str = string.Join(", ", suffixes); - _logger.LogDebug("Loaded suffixes ({suffixes}) from '{domain}'", str, fqdn); - } - - forestEntry.AddSuffix(suffixes); - } - - return forest; - } - - private IEnumerable LoadTrustedSchemas(ILdapConnection connection, ILdapForestLoader loader, LdapConnectionOptions connectionOptions, ILdapSchema mainSchema) - { - var trustedDomains = loader.LoadTrustedDomains(connection, mainSchema); - foreach (var trusted in trustedDomains) - { - var trustedFqdn = LdapNamesUtils.DnToFqdn(trusted); - _logger.LogDebug("Found trusted domain: '{trustedDomain}'", trustedFqdn); - var connectionString = connectionOptions.ConnectionString.CopySchemaAndPort(trustedFqdn); - var options = new LdapConnectionOptions( - connectionString, - connectionOptions.AuthType, - connectionOptions.Username, - connectionOptions.Password, - connectionOptions.Timeout); - - var trustedSchema = LoadSchema(options); - if (trustedSchema is not null) - yield return trustedSchema; - } - } - - private ILdapForestLoader? GetForestLoader(LdapImplementation ldapImplementation) - { - var loader = _ldapForestLoaderProvider.GetTrustedDomainsLoader(ldapImplementation); - return loader; - } - - private ILdapSchema? LoadSchema(LdapConnectionOptions connectionOptions) - { - var schema = _ldapSchemaLoader.Load(connectionOptions); - return schema; - } - - private IReadOnlyCollection? TryGetForestFromCache(string key) - { - _cache.TryGetValue(key, out IReadOnlyCollection? forest); - return forest; - } - - private string BuildCacheKey(string domain) - { - return "forest_" + domain; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapServerConfigurationService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapServerConfigurationService.cs deleted file mode 100644 index e1a74d9f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapServerConfigurationService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap.Forest; - -public class LdapServerConfigurationService : ILdapServerConfigurationService -{ - public IEnumerable DuplicateConfigurationForDn(IEnumerable targetDomains, ILdapServerConfiguration initialConfiguration) - { - return targetDomains.Select(x => CreateConfigurationWithDn(x, initialConfiguration)); - } - - private ILdapServerConfiguration CreateConfigurationWithDn(DistinguishedName trustedDomain, ILdapServerConfiguration initialConfiguration) - { - var connectionString = new LdapConnectionString(initialConfiguration.ConnectionString); - var trustedLdapDomain = LdapNamesUtils.DnToFqdn(trustedDomain); - var trustedConnectionString = connectionString.CopySchemaAndPort(trustedLdapDomain); - var config = new LdapServerConfiguration(trustedConnectionString.WellFormedLdapUrl, initialConfiguration.UserName, initialConfiguration.Password); - var settings = new LdapServerInitializeRequest(initialConfiguration); - config.Initialize(settings); - return config; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapGroupService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapGroupService.cs deleted file mode 100644 index 3c5065d3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapGroupService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapGroupService -{ - IReadOnlyList LoadUserGroups(LoadUserGroupsRequest request); - - bool IsMemberOf(MembershipRequest request); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapPasswordChanger.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapPasswordChanger.cs deleted file mode 100644 index aa35afcd..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapPasswordChanger.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapPasswordChanger -{ - Task ChangeUserPasswordAsync(string newPassword, ILdapProfile context); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileLoader.cs deleted file mode 100644 index 621469ab..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileLoader.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapProfileLoader -{ - public ILdapProfile? LoadLdapProfile( - string filter, - SearchScope scope = SearchScope.Subtree, - params LdapAttributeName[] attributeNames); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs deleted file mode 100644 index 39a00560..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapProfileService -{ - ILdapProfile? FindUserProfile(FindUserProfileRequest request); - Task ChangeUserPasswordAsync(ChangeUserPasswordRequest request); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemaLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemaLoader.cs deleted file mode 100644 index 15a23744..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemaLoader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapSchemaLoader -{ - ILdapSchema? Load(LdapConnectionOptions connectionOptions); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemeLoaderWrapper.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemeLoaderWrapper.cs deleted file mode 100644 index c571b7b2..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemeLoaderWrapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public interface ILdapSchemeLoaderWrapper -{ - ILdapSchema? Load(LdapConnectionOptions connectionOptions); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapGroupService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapGroupService.cs deleted file mode 100644 index be257df8..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapGroupService.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.LdapGroup.Load; -using Multifactor.Core.Ldap.LdapGroup.Membership; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class LdapGroupService : ILdapGroupService -{ - private readonly ILdapConnectionFactory _ldapConnectionFactory; - private readonly ILdapGroupLoaderFactory _ldapGroupLoaderFactory; - private readonly IMembershipCheckerFactory _ldapMembershipCheckerFactory; - - public LdapGroupService(ILdapGroupLoaderFactory ldapGroupLoaderFactory, IMembershipCheckerFactory ldapMembershipCheckerFactory, ILdapConnectionFactory ldapConnectionFactory) - { - _ldapGroupLoaderFactory = ldapGroupLoaderFactory; - _ldapMembershipCheckerFactory = ldapMembershipCheckerFactory; - _ldapConnectionFactory = ldapConnectionFactory; - } - - public IReadOnlyList LoadUserGroups(LoadUserGroupsRequest request) - { - ArgumentNullException.ThrowIfNull(request); - - var groupLoader = _ldapGroupLoaderFactory.GetGroupLoader(request.LdapSchema, request.LdapConnection, request.SearchBase ?? request.LdapSchema.NamingContext); - var groupDns = groupLoader.GetGroups(request.UserName, pageSize: 20); - return groupDns.Take(request.Limit).Select(x => x.Components.Deepest.Value).ToList(); - } - - public bool IsMemberOf(MembershipRequest request) - { - ArgumentNullException.ThrowIfNull(request); - if (request.TargetGroups.Count == 0) - throw new InvalidOperationException(); - - var isMemberOf = ProcessProfileGroups(request); - if (isMemberOf) - return true; - - if (!request.LoadNestedGroups) - return false; - - return ProcessNestedGroups(request); - } - - private bool ProcessProfileGroups(MembershipRequest request) - { - var intersection = request.ProfileGroups.Intersect(request.TargetGroups); - return intersection.Any(); - } - - private bool ProcessNestedGroups(MembershipRequest request) - { - using var connection = GetConnection(request); - return IsMemberOfNestedGroups(request, connection); - } - - private ILdapConnection GetConnection(MembershipRequest request) - { - var options = new LdapConnectionOptions( - request.ConnectionString, - AuthType.Basic, - request.UserName, - request.Password, - request.Timeout); - - return _ldapConnectionFactory.CreateConnection(options); - } - - private bool IsMemberOfNestedGroups(MembershipRequest request, ILdapConnection connection) => request.NestedGroupsBaseDns.Count > 0 - ? request.NestedGroupsBaseDns - .Select(groupBaseDn => IsMemberOf(request, connection, groupBaseDn)) - .Any(isMemberOf => isMemberOf) - : IsMemberOf(request, connection); - - private bool IsMemberOf(MembershipRequest request, ILdapConnection connection, DistinguishedName? searchBase = null) - { - var membershipChecker = _ldapMembershipCheckerFactory.GetMembershipChecker(request.LdapSchema, connection, searchBase ?? request.LdapSchema.NamingContext); - return membershipChecker.IsMemberOf(request.UserDn, request.TargetGroups.ToArray()); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapPasswordChanger.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapPasswordChanger.cs deleted file mode 100644 index 1ab2ac35..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapPasswordChanger.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.DirectoryServices.Protocols; -using System.Text; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class LdapPasswordChanger : ILdapPasswordChanger -{ - private readonly ILdapConnection _ldapConnection; - private readonly ILdapSchema _ldapSchema; - - public LdapPasswordChanger(ILdapConnection ldapConnection, ILdapSchema ldapSchema) - { - ArgumentNullException.ThrowIfNull(ldapConnection, nameof(ldapConnection)); - ArgumentNullException.ThrowIfNull(ldapSchema, nameof(ldapSchema)); - - _ldapConnection = ldapConnection; - _ldapSchema = ldapSchema; - } - - public Task ChangeUserPasswordAsync(string newPassword, ILdapProfile? profile) - { - try - { - if (profile is null) - return Task.FromResult(new PasswordChangeResponse() { Success = false, Message = "No user profile. Cannot change password." }); - - var userDn = profile.Dn; - var request = BuildPasswordChangeRequest(userDn, newPassword); - var response = _ldapConnection.SendRequest(request); - if (response.ResultCode != ResultCode.Success) - return Task.FromResult(new PasswordChangeResponse() { Success = false, Message = response.ErrorMessage }); - - return Task.FromResult(new PasswordChangeResponse() { Success = true }); - } - catch (Exception e) - { - return Task.FromResult(new PasswordChangeResponse() { Success = false, Message = e.Message }); - } - } - - private ModifyRequest BuildPasswordChangeRequest(DistinguishedName userDn, string newPassword) - { - var attributeName = _ldapSchema.LdapServerImplementation == LdapImplementation.ActiveDirectory - ? "unicodePwd" - : "userpassword"; - - var newPasswordAttribute = new DirectoryAttributeModification() - { - Name = attributeName, - Operation = DirectoryAttributeOperation.Replace - }; - if (_ldapSchema.LdapServerImplementation == LdapImplementation.ActiveDirectory) - newPasswordAttribute.Add(Encoding.Unicode.GetBytes($"\"{newPassword}\"")); - else - newPasswordAttribute.Add(newPassword); - - return new ModifyRequest(userDn.StringRepresentation, newPasswordAttribute); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileLoader.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileLoader.cs deleted file mode 100644 index f5f7fe62..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileLoader.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.DirectoryServices.Protocols; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class LdapProfileLoader : ILdapProfileLoader -{ - private readonly ILdapConnection _ldapConnection; - private readonly ILdapSchema _ldapSchema; - private readonly DistinguishedName _searchBase; - - public LdapProfileLoader(DistinguishedName searchBase, ILdapConnection ldapConnection, ILdapSchema ldapSchema) - { - Throw.IfNull(ldapConnection, nameof(ldapConnection)); - Throw.IfNull(ldapSchema, nameof(ldapSchema)); - - _ldapConnection = ldapConnection; - _ldapSchema = ldapSchema; - _searchBase = searchBase; - } - - public ILdapProfile? LoadLdapProfile( - string filter, - SearchScope scope = SearchScope.Subtree, - params LdapAttributeName[] attributeNames) - { - Throw.IfNullOrWhiteSpace(filter, nameof(filter)); - var result = _ldapConnection.Find(_searchBase, filter, scope, attributes: attributeNames); - var entry = result.FirstOrDefault(); - return entry is null ? null : new LdapProfile(entry, _ldapSchema); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs deleted file mode 100644 index 76e8434e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.DirectoryServices.Protocols; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; -using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class LdapProfileService : ILdapProfileService -{ - private readonly ILdapConnectionFactory _ldapConnectionFactory; - private readonly ILogger _logger; - - public LdapProfileService(ILdapConnectionFactory ldapConnectionFactory, ILogger logger) - { - _ldapConnectionFactory = ldapConnectionFactory; - _logger = logger; - } - - public ILdapProfile? FindUserProfile(FindUserProfileRequest request) - { - ArgumentNullException.ThrowIfNull(request); - - var options = GetLdapConnectionOptions(request.LdapServerConfiguration); - - using var connection = _ldapConnectionFactory.CreateConnection(options); - - var identityToSearch = request.UserIdentity; - if (request.UserIdentity.Format == UserIdentityFormat.NetBiosName) - { - var parts = new NetBiosParts(request.UserIdentity.Identity); - identityToSearch = new UserIdentity(parts.UserName); - } - - var filter = GetFilter(identityToSearch, request.LdapSchema); - _logger.LogDebug("Search base = '{searchBase}'. Filter for search = '{filter}'", request.SearchBase.StringRepresentation, filter); - var loader = new LdapProfileLoader(request.SearchBase, connection, request.LdapSchema); - return loader.LoadLdapProfile(filter, attributeNames: request.AttributeNames ?? []); - } - - public Task ChangeUserPasswordAsync(ChangeUserPasswordRequest request) - { - ArgumentNullException.ThrowIfNull(request, nameof(request)); - - var options = GetLdapConnectionOptions(request.ServerConfiguration); - - using var connection = _ldapConnectionFactory.CreateConnection(options); - var passwordChanger = new LdapPasswordChanger(connection, request.Schema); - return passwordChanger.ChangeUserPasswordAsync(request.NewPassword, request.Profile); - } - - private string GetFilter(UserIdentity identity, ILdapSchema schema) - { - var identityAttribute = GetIdentityAttribute(identity, schema); - var objectClass = schema.ObjectClass; - var classValue = schema.UserObjectClass; - - return $"(&({objectClass}={classValue})({identityAttribute}={identity.Identity}))"; - } - - private string GetIdentityAttribute(UserIdentity identity, ILdapSchema schema) => identity.Format switch - { - UserIdentityFormat.UserPrincipalName => "userPrincipalName", - UserIdentityFormat.DistinguishedName => schema.Dn, - UserIdentityFormat.SamAccountName => schema.Uid, - _ => throw new NotSupportedException("Unsupported user identity format") - }; - - private LdapConnectionOptions GetLdapConnectionOptions(ILdapServerConfiguration serverConfiguration) - { - return new LdapConnectionOptions( - new LdapConnectionString(serverConfiguration.ConnectionString), - AuthType.Basic, - serverConfiguration.UserName, - serverConfiguration.Password, - TimeSpan.FromSeconds(serverConfiguration.BindTimeoutInSeconds)); - } - - private class NetBiosParts - { - public string Netbios { get; set; } - public string UserName { get; set; } - - public NetBiosParts(string identity) - { - var index = identity.IndexOf('\\'); - if (index <= 0) - throw new ArgumentException($"Invalid NetBIOS identity: {identity}"); - - Netbios = identity[..index]; - UserName = identity[(index + 1)..]; - } - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapSchemaLoaderWrapper.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapSchemaLoaderWrapper.cs deleted file mode 100644 index 59312260..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapSchemaLoaderWrapper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -/// -/// Wrap LdapSchemaLoader from MF Core library -/// -public class LdapSchemaLoaderWrapper : ILdapSchemeLoaderWrapper -{ - private readonly LdapSchemaLoader _ldapSchemaLoader; - - public LdapSchemaLoaderWrapper(LdapSchemaLoader ldapSchemaLoader) - { - _ldapSchemaLoader = ldapSchemaLoader; - } - - public ILdapSchema? Load(LdapConnectionOptions connectionOptions) - { - ArgumentNullException.ThrowIfNull(connectionOptions); - - return _ldapSchemaLoader.Load(connectionOptions); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LoadUserGroupsRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LoadUserGroupsRequest.cs deleted file mode 100644 index b7f9213b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LoadUserGroupsRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class LoadUserGroupsRequest -{ - public ILdapSchema LdapSchema { get; } - public ILdapConnection LdapConnection { get; } - public DistinguishedName UserName { get; } - public DistinguishedName? SearchBase { get; } - public int Limit { get; } - - public LoadUserGroupsRequest(ILdapSchema ldapSchema, ILdapConnection ldapConnection, DistinguishedName userName, DistinguishedName? searchBase = null, int limit = int.MaxValue) - { - ArgumentNullException.ThrowIfNull(ldapSchema); - ArgumentNullException.ThrowIfNull(ldapConnection); - ArgumentNullException.ThrowIfNull(userName); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit, nameof(limit)); - - LdapSchema = ldapSchema; - LdapConnection = ldapConnection; - UserName = userName; - SearchBase = searchBase; - Limit = limit; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/MembershipRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/MembershipRequest.cs deleted file mode 100644 index 817a9e0c..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/MembershipRequest.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class MembershipRequest -{ - public DistinguishedName UserDn { get; } - public IReadOnlyCollection ProfileGroups { get; } - public bool LoadNestedGroups { get; } - public IReadOnlyCollection NestedGroupsBaseDns { get; } - public LdapConnectionString ConnectionString { get; } - public string UserName { get; } - public string Password { get; } - public TimeSpan Timeout { get; } - public ILdapSchema LdapSchema { get; } - public IReadOnlyCollection TargetGroups { get; } - - public MembershipRequest(IRadiusPipelineExecutionContext context, IReadOnlyCollection targetGroups) - { - ArgumentNullException.ThrowIfNull(context.UserLdapProfile); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration); - ArgumentNullException.ThrowIfNull(context.LdapSchema); - ArgumentNullException.ThrowIfNull(targetGroups); - - UserDn = context.UserLdapProfile.Dn; - ProfileGroups = context.UserLdapProfile.MemberOf; - LoadNestedGroups = context.LdapServerConfiguration.LoadNestedGroups; - NestedGroupsBaseDns = context.LdapServerConfiguration.NestedGroupsBaseDns; - UserName = context.LdapServerConfiguration.UserName; - Password = context.LdapServerConfiguration.Password; - Timeout = TimeSpan.FromSeconds(context.LdapServerConfiguration.BindTimeoutInSeconds); - ConnectionString = new LdapConnectionString(context.LdapServerConfiguration.ConnectionString); - LdapSchema = context.LdapSchema; - TargetGroups = targetGroups; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeRequest.cs deleted file mode 100644 index f553435e..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class PasswordChangeRequest -{ - public string Id { get; private set; } = Guid.NewGuid().ToString(); - - public string Domain { get; set; } = string.Empty; - - public string? CurrentPasswordEncryptedData { get; set; } - - public string? NewPasswordEncryptedData { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeResponse.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeResponse.cs deleted file mode 100644 index f273f02c..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Ldap; - -public class PasswordChangeResponse -{ - public bool Success { get; set; } - - public string? Message { get; set; } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/CreateSecondFactorRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/CreateSecondFactorRequest.cs deleted file mode 100644 index 3ff8acaa..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/CreateSecondFactorRequest.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -public class CreateSecondFactorRequest -{ - public ILdapProfile? UserProfile { get; } - public IRadiusPacket RequestPacket { get; } - public IPEndPoint RemoteEndpoint { get; } - public string ConfigName { get; } - public PrivacyModeDescriptor PrivacyMode { get; } - public AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; } - public string? SignUpGroups { get; } - public UserPassphrase Passphrase { get; } - public PreAuthModeDescriptor PreAuthnMode { get; } - public AuthenticationSource FirstFactorAuthenticationSource { get; } - public UserNameTransformRules UserNameTransformRules { get; } - public ApiCredential ApiCredential { get; } - public string? IdentityAttribute { get; } - public bool BypassSecondFactorWhenApiUnreachable { get; } - public IReadOnlyList PhoneAttributesNames { get; } - public IReadOnlyList ApiUrls { get; } - public bool ApiResponseCacheEnabled { get; } - - public CreateSecondFactorRequest(IRadiusPipelineExecutionContext context, bool cacheEnabled = true) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.RequestPacket); - ArgumentNullException.ThrowIfNull(context.RemoteEndpoint); - ArgumentException.ThrowIfNullOrWhiteSpace(context.ClientConfigurationName); - ArgumentNullException.ThrowIfNull(context.AuthenticationCacheLifetime); - ArgumentNullException.ThrowIfNull(context.PrivacyMode); - ArgumentNullException.ThrowIfNull(context.Passphrase); - ArgumentNullException.ThrowIfNull(context.PreAuthnMode); - ArgumentNullException.ThrowIfNull(context.UserNameTransformRules); - ArgumentNullException.ThrowIfNull(context.ApiCredential); - - UserProfile = context.UserLdapProfile; - RequestPacket = context.RequestPacket; - RemoteEndpoint = context.RemoteEndpoint; - ConfigName = context.ClientConfigurationName; - AuthenticationCacheLifetime = context.AuthenticationCacheLifetime; - PrivacyMode = context.PrivacyMode; - SignUpGroups = context.SignUpGroups; - Passphrase = context.Passphrase; - PreAuthnMode = context.PreAuthnMode; - FirstFactorAuthenticationSource = context.FirstFactorAuthenticationSource; - UserNameTransformRules = context.UserNameTransformRules; - ApiCredential = context.ApiCredential; - IdentityAttribute = context.LdapServerConfiguration?.IdentityAttribute; - BypassSecondFactorWhenApiUnreachable = context.BypassSecondFactorWhenApiUnreachable; - PhoneAttributesNames = context.LdapServerConfiguration?.PhoneAttributes ?? new List(); - ApiResponseCacheEnabled = cacheEnabled; - ApiUrls = context.ApiUrls; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApi.cs deleted file mode 100644 index b29fe0cf..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApi.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -public interface IMultifactorApi -{ - Task CreateAccessRequest(string address, AccessRequest payload, ApiCredential apiCredentials); - Task SendChallengeAsync(string address, ChallengeRequest payload, ApiCredential apiCredentials); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApiService.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApiService.cs deleted file mode 100644 index 29793d7b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApiService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -public interface IMultifactorApiService -{ - Task CreateSecondFactorRequestAsync(CreateSecondFactorRequest context); - Task SendChallengeAsync(SendChallengeRequest request); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApi.cs deleted file mode 100644 index 0405eea3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApi.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Net; -using System.Text; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Http; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -public class MultifactorApi : IMultifactorApi -{ - private readonly ILogger _logger; - private readonly IHttpClient _httpClient; - - public MultifactorApi(IHttpClient httpClient, ILogger logger) - { - ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient)); - ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - - _logger = logger; - _httpClient = httpClient; - } - - public Task CreateAccessRequest(string address, AccessRequest payload, ApiCredential apiCredentials) - { - ArgumentNullException.ThrowIfNull(payload, nameof(payload)); - ArgumentNullException.ThrowIfNull(apiCredentials, nameof(apiCredentials)); - ArgumentException.ThrowIfNullOrWhiteSpace(address); - - return SendRequestAsync($"{address}/access/requests/ra", payload, apiCredentials); - } - - public Task SendChallengeAsync(string address, ChallengeRequest payload, ApiCredential apiCredentials) - { - ArgumentNullException.ThrowIfNull(payload, nameof(payload)); - ArgumentNullException.ThrowIfNull(apiCredentials, nameof(apiCredentials)); - ArgumentException.ThrowIfNullOrWhiteSpace(address); - - return SendRequestAsync($"{address}/access/requests/ra/challenge", payload, apiCredentials); - } - - private async Task SendRequestAsync(string url, object payload, ApiCredential credentials) - { - var trace = $"rds-{Guid.NewGuid()}"; - using var scope = _logger.BeginScope(new Dictionary(1) { { "mf-trace-id", trace } }); - var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{credentials.Usr}:{credentials.Pwd}")); - var headers = new Dictionary - { - { "Authorization", $"Basic {auth}" }, - { "mf-trace-id", trace } - }; - - try - { - return await SendAsync(url, payload, headers); - } - catch (HttpRequestException ex) - { - return ProcessHttpRequestException(ex, url); - } - catch (TaskCanceledException) - { - _logger.LogWarning("Multifactor API timeout expired."); - return new AccessRequestResponse() { Status = RequestStatus.Denied }; - } - catch (Exception ex) - { - throw new MultifactorApiUnreachableException( - $"Multifactor API host unreachable: {url}. Reason: {ex.Message}", ex); - } - } - - private async Task SendAsync(string url, object payload, Dictionary headers) - { - var response = await _httpClient.PostAsync>(url, payload, headers); - - if (response is null) - return new AccessRequestResponse() - { - Status = RequestStatus.Denied, - ReplyMessage = "Empty response", - }; - - if (!response.Success) - { - _logger.LogWarning("Got unsuccessful response from API: {@response}", response); - } - - return response.Model; - } - - private AccessRequestResponse ProcessHttpRequestException(HttpRequestException ex, string url) - { - if (ex.StatusCode != HttpStatusCode.TooManyRequests) - { - throw new MultifactorApiUnreachableException( - $"Multifactor API host unreachable: {url}. Reason: {ex.Message}", ex); - } - - _logger.LogWarning("Unsuccessful api response: '{message:l}'", ex.Message); - return new AccessRequestResponse() - { - Status = RequestStatus.Denied, - ReplyMessage = "Too Many Requests" - }; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApiService.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApiService.cs deleted file mode 100644 index ce8a64d0..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApiService.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi.PrivacyMode; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -//TODO separate creation and sending api request -public class MultifactorApiService : IMultifactorApiService -{ - private readonly IMultifactorApi _api; - private readonly IAuthenticatedClientCache _authenticatedClientCache; - private readonly ILogger _logger; - - public MultifactorApiService(IMultifactorApi api, IAuthenticatedClientCache authenticatedClientCache, ILogger logger) - { - ArgumentNullException.ThrowIfNull(api, nameof(api)); - ArgumentNullException.ThrowIfNull(authenticatedClientCache, nameof(authenticatedClientCache)); - ArgumentNullException.ThrowIfNull(logger, nameof(logger)); - - _api = api; - _authenticatedClientCache = authenticatedClientCache; - _logger = logger; - } - - public async Task CreateSecondFactorRequestAsync(CreateSecondFactorRequest request) - { - ArgumentNullException.ThrowIfNull(request, nameof(request)); - var secondFactorIdentity = GetSecondFactorIdentity(request); - if (string.IsNullOrWhiteSpace(secondFactorIdentity)) - { - _logger.LogWarning("Empty user name for second factor request. Request rejected."); - return new MultifactorResponse(AuthenticationStatus.Reject); - } - - var personalData = GetPersonalData(request); - - //try to get authenticated client to bypass second factor if configured - if (_authenticatedClientCache.TryHitCache(personalData.CallingStationId, personalData.Identity, request.ConfigName, request.AuthenticationCacheLifetime)) - { - _logger.LogInformation( - "Bypass second factor for user '{user:l}' with calling-station-id {csi:l} from {host:l}:{port}", - personalData.Identity, - personalData.CallingStationId, - request.RemoteEndpoint.Address, - request.RemoteEndpoint.Port); - return new MultifactorResponse(AuthenticationStatus.Bypass); - } - - ApplyPrivacyMode(personalData, request.PrivacyMode); - var payload = GetRequestPayload(personalData, request); - - MultifactorResponse cloudResponse = new MultifactorResponse(AuthenticationStatus.Reject); - foreach (var apiUrl in request.ApiUrls) - { - // TODO move to method - try - { - var response = await CreateAccessRequestAsync(apiUrl, payload, request.ApiCredential); - var responseCode = ConvertToAuthCode(response); - - if (responseCode == AuthenticationStatus.Reject) - { - var reason = response?.ReplyMessage; - var phone = response?.Phone; - _logger.LogWarning( - "Second factor verification for user '{user:l}' from {host:l}:{port} failed with reason='{reason:l}'. User phone {phone:l}", - personalData.Identity, - request.RemoteEndpoint.Address, - request.RemoteEndpoint.Port, - reason, - phone); - } - - var mfResponse = new MultifactorResponse(responseCode, state: response?.Id, replyMessage: response?.ReplyMessage); - - if (!ShouldCacheResponse(request.ApiResponseCacheEnabled, responseCode, response)) - { - _logger.LogDebug("Skip 2FA response caching for user '{user}'.", request.RequestPacket.UserName); - return mfResponse; - } - - LogGrantedInfo(personalData.Identity, response, request.RequestPacket.CallingStationIdAttribute); - _authenticatedClientCache.SetCache(personalData.CallingStationId, personalData.Identity, request.ConfigName, request.AuthenticationCacheLifetime); - - return mfResponse; - } - catch (MultifactorApiUnreachableException apiEx) - { - cloudResponse = ProcessMfException(apiEx, personalData.Identity, - request.BypassSecondFactorWhenApiUnreachable, request.RemoteEndpoint); - } - catch (Exception ex) - { - cloudResponse = ProcessException(ex, personalData.Identity, request.RemoteEndpoint); - } - } - - if (cloudResponse.Code == AuthenticationStatus.Bypass) - _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", personalData.Identity, request.RemoteEndpoint.Address, request.RemoteEndpoint.Port); - - return cloudResponse; - } - - public async Task SendChallengeAsync(SendChallengeRequest request) - { - ArgumentNullException.ThrowIfNull(request, nameof(request)); - ArgumentException.ThrowIfNullOrWhiteSpace(request.RequestId, nameof(request.RequestId)); - ArgumentException.ThrowIfNullOrWhiteSpace(request.Answer, nameof(request.Answer)); - - var identity = GetSecondFactorIdentity(request.IdentityAttribute, request.RequestPacket.UserName, request.UserProfile?.Attributes ?? []); - - if (string.IsNullOrWhiteSpace(identity)) - throw new InvalidOperationException("The identity is empty."); - - var payload = new ChallengeRequest() - { - Identity = identity, - Challenge = request.Answer, - RequestId = request.RequestId - }; - - var callingStationIdAttr = request.RequestPacket.CallingStationIdAttribute; - var callingStationId = GetCallingStationId(callingStationIdAttr, request.RemoteEndpoint); - MultifactorResponse cloudResponse = new MultifactorResponse(AuthenticationStatus.Reject); - - foreach (var apiUrl in request.ApiUrls) - { - // TODO move to method - try - { - var response = await _api.SendChallengeAsync(apiUrl, payload, request.ApiCredential); - var responseCode = ConvertToAuthCode(response); - - var mfResponse = new MultifactorResponse(responseCode, state: response?.Id, replyMessage: response?.ReplyMessage); - - if (!ShouldCacheResponse(request.ApiResponseCacheEnabled, responseCode, response)) - { - _logger.LogDebug("Skip challenge response caching for user '{user}'.", request.RequestPacket.UserName); - return mfResponse; - } - - LogGrantedInfo(identity, response, callingStationId); - _authenticatedClientCache.SetCache(callingStationId, identity, request.ConfigName, request.AuthenticationCacheLifetime); - - return mfResponse; - } - catch (MultifactorApiUnreachableException apiEx) - { - cloudResponse = ProcessMfException(apiEx, identity, request.BypassSecondFactorWhenApiUnreachable, request.RemoteEndpoint); - } - catch (Exception ex) - { - cloudResponse = ProcessException(ex, identity, request.RemoteEndpoint); - } - } - - if (cloudResponse.Code == AuthenticationStatus.Bypass) - _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", identity, - request.RemoteEndpoint.Address, request.RemoteEndpoint.Port); - - return cloudResponse; - } - - private AuthenticationStatus ConvertToAuthCode(AccessRequestResponse? multifactorAccessRequest) - { - if (multifactorAccessRequest == null) - return AuthenticationStatus.Reject; - - switch (multifactorAccessRequest.Status) - { - case RequestStatus.Granted when multifactorAccessRequest.Bypassed: - return AuthenticationStatus.Bypass; - - case RequestStatus.Granted: - return AuthenticationStatus.Accept; - - case RequestStatus.Denied: - return AuthenticationStatus.Reject; - - case RequestStatus.AwaitingAuthentication: - return AuthenticationStatus.Awaiting; - - default: - _logger.LogWarning("Got unexpected status from API: {status:l}", multifactorAccessRequest.Status); - return AuthenticationStatus.Reject; - } - } - - private void LogGrantedInfo(string identity, AccessRequestResponse? response, string? callingStationIdAttribute) - { - string? countryValue = null; - string? regionValue = null; - string? cityValue = null; - var callingStationId = callingStationIdAttribute; - - if (response != null && IPAddress.TryParse(callingStationId, out var ip)) - { - countryValue = response.CountryCode; - regionValue = response.Region; - cityValue = response.City; - callingStationId = ip.ToString(); - } - - _logger.LogInformation( - "Second factor for user '{user:l}' verified successfully. Authenticator: '{authenticator:l}', account: '{account:l}', country: '{country:l}', region: '{region:l}', city: '{city:l}', calling-station-id: {clientIp}, authenticatorId: {authenticatorId}", - identity, - response?.Authenticator, - response?.Account, - countryValue, - regionValue, - cityValue, - callingStationId, - response?.AuthenticatorId); - } - - private static string? GetPassCodeOrNull(CreateSecondFactorRequest context) - { - //check static challenge - var challenge = context.RequestPacket.TryGetChallenge(); - if (challenge != null) - { - return challenge; - } - - //check password challenge (otp or passcode) - var passphrase = context.Passphrase; - switch (context.PreAuthnMode.Mode) - { - case PreAuthMode.Otp: - return passphrase.Otp; - } - - if (passphrase.IsEmpty) - return null; - - if (context.FirstFactorAuthenticationSource != AuthenticationSource.None) - return null; - - return passphrase.Otp ?? passphrase.ProviderCode; - } - - private async Task CreateAccessRequestAsync(string address, AccessRequest payload, ApiCredential apiCredential) - { - var response = await _api.CreateAccessRequest(address, payload, apiCredential); - return response; - } - - private string? GetSecondFactorIdentity(CreateSecondFactorRequest context) - { - if (string.IsNullOrWhiteSpace(context.IdentityAttribute)) - return context.RequestPacket.UserName; - - return context.UserProfile?.Attributes - .FirstOrDefault(x => x.Name == context.IdentityAttribute)?.Values - .FirstOrDefault(); - } - - private string? GetSecondFactorIdentity(string? identityAttribute, string? userName, - IReadOnlyCollection profileAttributes) - { - if (string.IsNullOrWhiteSpace(identityAttribute)) - return userName; - - return profileAttributes - .FirstOrDefault(x => x.Name == identityAttribute)?.Values - .FirstOrDefault(); - } - - private PersonalData GetPersonalData(CreateSecondFactorRequest request) - { - var secondFactorIdentity = GetSecondFactorIdentity(request); - var callingStationId = request.RequestPacket.CallingStationIdAttribute; - - var callingStationIdForApiRequest = GetCallingStationId(callingStationId, request.RemoteEndpoint); - - var phone = request.UserProfile?.Attributes - .Where(x => request.PhoneAttributesNames.Contains(x.Name.Value)) - .Select(x => x.GetNotEmptyValues().FirstOrDefault()) - .FirstOrDefault(); - - var personalData = new PersonalData - { - Identity = secondFactorIdentity!, - DisplayName = request.UserProfile?.DisplayName, - Email = request.UserProfile?.Email, - Phone = string.IsNullOrWhiteSpace(phone) ? request.UserProfile?.Phone : phone, - CalledStationId = request.RequestPacket.CalledStationIdAttribute, - CallingStationId = callingStationIdForApiRequest - }; - - return personalData; - } - - private string? GetCallingStationId(string? callingStationIdAttributeValue, IPEndPoint remoteEndPoint) - { - // CallingStationId may contain hostname. For IP policy to work correctly in MF cloud we need IP instead of hostname - return IPAddress.TryParse(callingStationIdAttributeValue ?? string.Empty, out _) - ? callingStationIdAttributeValue - : remoteEndPoint.Address.ToString(); - } - - private AccessRequest GetRequestPayload(PersonalData personalData, CreateSecondFactorRequest context) - { - return new AccessRequest - { - Identity = UserNameTransformation.Transform(personalData.Identity, context.UserNameTransformRules.BeforeSecondFactor), - Name = personalData.DisplayName, - Email = personalData.Email, - Phone = personalData.Phone, - PassCode = GetPassCodeOrNull(context), - CallingStationId = personalData.CallingStationId, - CalledStationId = personalData.CalledStationId, - Capabilities = new Capabilities - { - InlineEnroll = true - }, - GroupPolicyPreset = new GroupPolicyPreset - { - SignUpGroups = context.SignUpGroups ?? string.Empty - } - }; - } - - private void ApplyPrivacyMode(PersonalData pd, PrivacyModeDescriptor modeDescriptor) - { - //remove user information for privacy - switch (modeDescriptor.Mode) - { - case PrivacyMode.Full: - pd.DisplayName = null; - pd.Email = null; - pd.Phone = null; - pd.CallingStationId = ""; - pd.CalledStationId = null; - break; - - case PrivacyMode.Partial: - if (!modeDescriptor.HasField("Name")) - pd.DisplayName = null; - - if (!modeDescriptor.HasField("Email")) - pd.Email = null; - - if (!modeDescriptor.HasField("Phone")) - pd.Phone = null; - - if (!modeDescriptor.HasField("RemoteHost")) - pd.CallingStationId = ""; - - pd.CalledStationId = null; - - break; - } - } - - private MultifactorResponse ProcessMfException(MultifactorApiUnreachableException apiEx, string identity, bool bypassSecondFactorWhenApiUnreachable, IPEndPoint remoteEndpoint) - { - _logger.LogError(apiEx, - "Error occured while requesting API for user '{user:l}' from {host:l}:{port}, {msg:l}", - identity, - remoteEndpoint.Address, - remoteEndpoint.Port, - apiEx.Message); - - if (!bypassSecondFactorWhenApiUnreachable) - { - var radCode = ConvertToAuthCode(null); - return new MultifactorResponse(radCode); - } - - var code = ConvertToAuthCode(AccessRequestResponse.Bypass); - return new MultifactorResponse(code); - } - - private MultifactorResponse ProcessException(Exception ex, string identity, IPEndPoint remoteEndpoint) - { - _logger.LogError(ex, "Error occured while requesting API for user '{user:l}' from {host:l}:{port}, {msg:l}", - identity, - remoteEndpoint.Address, - remoteEndpoint.Port, - ex.Message); - - var code = ConvertToAuthCode(null); - return new MultifactorResponse(code); - } - - private bool ShouldCacheResponse(bool apiResponseCacheEnabled, AuthenticationStatus responseCode, AccessRequestResponse? response) => apiResponseCacheEnabled && responseCode == AuthenticationStatus.Accept && !(response?.Bypassed ?? false); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/SendChallengeRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/SendChallengeRequest.cs deleted file mode 100644 index ae4d14c5..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/SendChallengeRequest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Services.MultifactorApi; - -public class SendChallengeRequest -{ - public ApiCredential ApiCredential { get; } - public ILdapProfile? UserProfile { get; } - public string? IdentityAttribute { get; } - public IRadiusPacket RequestPacket { get; } - public string ConfigName { get; } - public AuthenticatedClientCacheConfig AuthenticationCacheLifetime { get; } - public bool BypassSecondFactorWhenApiUnreachable { get; } - public IPEndPoint RemoteEndpoint { get; } - public string Answer { get; } - public string RequestId { get; } - public bool ApiResponseCacheEnabled { get; } - public IReadOnlyList ApiUrls { get; } - - public SendChallengeRequest(IRadiusPipelineExecutionContext context, string answer, string requestId, bool cacheEnabled = true) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.ApiCredential); - ArgumentNullException.ThrowIfNull(context.RequestPacket); - ArgumentException.ThrowIfNullOrWhiteSpace(context.ClientConfigurationName); - ArgumentException.ThrowIfNullOrWhiteSpace(answer); - ArgumentException.ThrowIfNullOrWhiteSpace(requestId); - ArgumentNullException.ThrowIfNull(context.AuthenticationCacheLifetime); - ArgumentNullException.ThrowIfNull(context.RemoteEndpoint); - - ApiCredential = context.ApiCredential; - IdentityAttribute = context.LdapServerConfiguration?.IdentityAttribute; - UserProfile = context.UserLdapProfile; - RequestPacket = context.RequestPacket; - ConfigName = context.ClientConfigurationName; - AuthenticationCacheLifetime = context.AuthenticationCacheLifetime; - BypassSecondFactorWhenApiUnreachable = context.BypassSecondFactorWhenApiUnreachable; - RemoteEndpoint = context.RemoteEndpoint; - Answer = answer; - RequestId = requestId; - ApiResponseCacheEnabled = cacheEnabled; - ApiUrls = context.ApiUrls; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/GetReplyAttributesRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/GetReplyAttributesRequest.cs deleted file mode 100644 index caaf5819..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/GetReplyAttributesRequest.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Globalization; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public class GetReplyAttributesRequest -{ - public IReadOnlyDictionary ReplyAttributes { get; } - public HashSet UserGroups { get; } - private IReadOnlyCollection Attributes { get; } - public string? UserName { get; } - - public GetReplyAttributesRequest( - string? userName, - HashSet userGroups, - IReadOnlyDictionary replyAttributes, - IReadOnlyCollection attributes) - { - ArgumentNullException.ThrowIfNull(userGroups); - ArgumentNullException.ThrowIfNull(replyAttributes); - ArgumentNullException.ThrowIfNull(attributes); - - UserName = userName; - UserGroups = userGroups; - ReplyAttributes = replyAttributes; - Attributes = attributes; - } - - public string? GetAttributeFirstValue(string attributeName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(attributeName, nameof(attributeName)); - - var name = ToLower(attributeName); - var attribute = Attributes.FirstOrDefault(x => ToLower(x.Name) == name); - return attribute?.GetNotEmptyValues().FirstOrDefault(); - } - - public IReadOnlyCollection GetAttributeValues(string attributeName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(attributeName, nameof(attributeName)); - - var name = ToLower(attributeName); - var attribute = Attributes.FirstOrDefault(x => ToLower(x.Name) == name); - return attribute?.GetNotEmptyValues() ?? []; - } - - public bool HasAttribute(string attributeName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(attributeName, nameof(attributeName)); - var attribute = Attributes.FirstOrDefault(x => ToLower(x.Name) == ToLower(attributeName)); - return attribute is not null; - } - - private static string ToLower(string s) => s.ToLower(CultureInfo.InvariantCulture); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusAttributeTypeConverter.cs deleted file mode 100644 index 69ea7910..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusAttributeTypeConverter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public interface IRadiusAttributeTypeConverter -{ - object ConvertType(string attrName, object value); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusPacketService.cs deleted file mode 100644 index 2963ecdb..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusPacketService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public interface IRadiusPacketService -{ - IRadiusPacket Parse(byte[] packetBytes, SharedSecret sharedSecret, RadiusAuthenticator requestAuthenticator = null); - RadiusPacket CreateResponsePacket(IRadiusPacket radiusPacket, PacketCode responsePacketCode); - byte[] GetBytes(IRadiusPacket packet, SharedSecret sharedSecret); - bool TryGetNasIdentifier(byte[] packetBytes, out string nasIdentifier); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusReplyAttributeService.cs deleted file mode 100644 index c2eaf0b5..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusReplyAttributeService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public interface IRadiusReplyAttributeService -{ - IDictionary> GetReplyAttributes(GetReplyAttributesRequest request); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusAttributeTypeConverter.cs deleted file mode 100644 index bbcbcb90..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusAttributeTypeConverter.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Globalization; -using System.Net; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public class RadiusAttributeTypeConverter : IRadiusAttributeTypeConverter -{ - private readonly IRadiusDictionary _dictionary; - - public RadiusAttributeTypeConverter(IRadiusDictionary dictionary) - { - _dictionary = dictionary; - } - - public object ConvertType(string attrName, object value) - { - if (value is not string stringValue) - return value; - - var attribute = _dictionary.GetAttribute(attrName); - switch (attribute.Type) - { - case "ipaddr": - if (IPAddress.TryParse(stringValue, out var ipValue)) - return ipValue; - - // maybe it is msRADIUSFramedIPAddress value - if (int.TryParse(stringValue, out var val)) - return MsRadiusFramedIpAddressToIpAddress(val); - - break; - case "date": - if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateValue)) - return dateValue; - break; - case "integer": - if (int.TryParse(stringValue, out var integerValue)) - return integerValue; - break; - } - - return value; - } - - private IPAddress MsRadiusFramedIpAddressToIpAddress(int intValue) - { - long longValue = intValue; - - // Microsoft subtracts 4294967296 from numbers above 2147483647 to - // make them negative to make it, sort of, unsigned. - // https://document.phenixid.net/m/90910/l/1601121-how-to-setup-framed-ip-using-ad-with-msradiusframedipaddress-attribute - if (longValue < 0) - { - longValue += 4294967296; - } - - var bytes = BitConverter.GetBytes(longValue).Take(4).ToArray(); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } - - return new IPAddress(bytes); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClient.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClient.cs deleted file mode 100644 index 992c594b..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClient.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Concurrent; -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public sealed class RadiusClient : IRadiusClient -{ - private readonly UdpClient _udpClient; - - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary, TaskCompletionSource> _pendingRequests = new(); - - private readonly CancellationTokenSource _cancellationTokenSource; - private readonly ILogger _logger; - - /// - /// Create a radius client which sends and receives responses on localEndpoint - /// - public RadiusClient(IPEndPoint localEndpoint, ILogger logger) - { - Throw.IfNull(localEndpoint); - Throw.IfNull(logger); - _logger = logger; - _udpClient = new UdpClient(localEndpoint); - - _cancellationTokenSource = new CancellationTokenSource(); - - var receiveTask = StartReceiveLoopAsync(_cancellationTokenSource.Token); - } - - - /// - /// Send a packet with specified timeout - /// - public async Task SendPacketAsync(byte identifier, byte[] requestPacket, IPEndPoint remoteEndpoint, TimeSpan timeout) - { - var responseTaskCs = new TaskCompletionSource(); - - if (_pendingRequests.TryAdd(new Tuple(identifier, remoteEndpoint), responseTaskCs)) - { - await _udpClient.SendAsync(requestPacket, requestPacket.Length, remoteEndpoint); - var completedTask = await Task.WhenAny(responseTaskCs.Task, Task.Delay(timeout)); - if (completedTask == responseTaskCs.Task) - { - return responseTaskCs.Task.Result.Buffer; - } - - //timeout - _logger.LogDebug("Server {remoteEndpoint:l} did not respond within {timeout:l}", remoteEndpoint, timeout.ToString()); - return null; - } - - _logger.LogWarning("Network error"); - return null; - } - - /// - /// Receive packets in a loop and complete tasks based on identifier - /// - private async Task StartReceiveLoopAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var response = await _udpClient.ReceiveAsync(cancellationToken); - - if (_pendingRequests.TryRemove(new Tuple(response.Buffer[1], response.RemoteEndPoint), out var taskCs)) - taskCs.SetResult(response); - } - catch (ObjectDisposedException) - { - // This is thrown when udpclient is disposed, can be safely ignored - } - catch (TaskCanceledException) - { - - } - - await Task.Delay(TimeSpan.FromMilliseconds(5), cancellationToken); - } - } - - public void Dispose() - { - _cancellationTokenSource.Cancel(); - _udpClient?.Close(); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketNasIdentifierParser.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketNasIdentifierParser.cs deleted file mode 100644 index de20a049..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketNasIdentifierParser.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Text; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius -{ - // Simple radius parser to extract NAS-Identifier attrbute - public static class RadiusPacketNasIdentifierParser - { - private const int NasIdentifierAttributeCode = 32; - - public static bool TryParse(byte[] packetBytes, out string? nasIdentifier) - { - nasIdentifier = null; - - var packetLength = BitConverter.ToUInt16(packetBytes.Skip(2).Take(2).Reverse().ToArray(), 0); - if (packetBytes.Length != packetLength) - { - throw new InvalidOperationException($"Packet length does not match, expected: {packetLength}, actual: {packetBytes.Length}"); - } - - var position = 20; - while (position < packetBytes.Length) - { - var typecode = packetBytes[position]; - var length = packetBytes[position + 1]; - - if (position + length > packetLength) - { - throw new ArgumentOutOfRangeException("Invalid packet length"); - } - - if (typecode == NasIdentifierAttributeCode) - { - var contentBytes = new byte[length - 2]; - Buffer.BlockCopy(packetBytes, position + 2, contentBytes, 0, length - 2); - - nasIdentifier = Encoding.UTF8.GetString(contentBytes); - - return true; - } - - position += length; - } - - return false; - } - - } -} diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketService.cs deleted file mode 100644 index 4a11c86a..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketService.cs +++ /dev/null @@ -1,492 +0,0 @@ -using System.Net; -using System.Security.Cryptography; -using System.Text; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Core.Radius.Metadata; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -// See https://datatracker.ietf.org/doc/html/rfc2865#section-3 -public class RadiusPacketService : IRadiusPacketService -{ - private readonly ILogger _logger; - private readonly IRadiusDictionary _radiusDictionary; - private const int NasIdentifierAttributeCode = 32; - - public RadiusPacketService(ILogger logger, IRadiusDictionary radiusDictionary) - { - _logger = logger; - _radiusDictionary = radiusDictionary; - } - - public IRadiusPacket Parse( - byte[] packetBytes, - SharedSecret sharedSecret, - RadiusAuthenticator requestAuthenticator = null) - { - if (packetBytes.Length < RadiusFieldOffsets.LengthFieldPosition + RadiusFieldOffsets.LengthFieldLength) - { - throw new InvalidOperationException($"Packet too short: {packetBytes.Length}"); - } - - ushort packetLength = GetPacketLength(packetBytes); - if (packetBytes.Length != packetLength) - { - throw new InvalidOperationException( - $"Packet length does not match, expected: {packetLength}, actual: {packetBytes.Length}"); - } - - var header = RadiusPacketHeader.Parse(packetBytes); - var packet = new RadiusPacket(header, requestAuthenticator); - - if (packet.Code == PacketCode.AccountingRequest || packet.Code == PacketCode.DisconnectRequest) - { - var requestAuth = CalculateRequestAuthenticator(sharedSecret, packetBytes); - if (!packet.Authenticator.Value.SequenceEqual(requestAuth)) - { - throw new InvalidOperationException( - $"Invalid request authenticator in packet {packet.Identifier}, check secret?"); - } - } - - var position = RadiusFieldOffsets.AttributesFieldPosition; - var messageAuthenticatorPosition = 0; - - // see https://datatracker.ietf.org/doc/html/rfc2865#section-5 - while (position < packetBytes.Length) - { - var typeCode = packetBytes[position]; - var length = packetBytes[position + 1]; - - if (position + length > packetLength) - { - throw new ArgumentOutOfRangeException(); - } - - var contentBytes = new byte[length - 2]; - Buffer.BlockCopy(packetBytes, position + 2, contentBytes, 0, length - 2); - - try - { - AttributeValue? attribute = null; - if (typeCode == RadiusAttributeCode.VendorSpecific) - attribute = ParseVendorSpecificAttribute(contentBytes, typeCode, packet.Authenticator, sharedSecret); - else - attribute = ParseAttribute(contentBytes, typeCode, packet.Authenticator, sharedSecret); - - if (attribute != null) - { - packet.AddAttributeValue(attribute.Name, attribute!.Value); - if (attribute.IsMessageAuthenticator) - messageAuthenticatorPosition = position; - } - } - catch (KeyNotFoundException) - { - _logger.LogWarning("Attribute {typecode:l} not found in dictionary", typeCode); - } - catch (Exception ex) - { - _logger.LogError(ex, "Something went wrong parsing attribute {typecode:l}", typeCode); - } - - position += length; - } - - if (messageAuthenticatorPosition != 0) - { - var messageAuthenticator = packet.GetAttribute("Message-Authenticator"); - - if (!IsMessageAuthenticatorValid( - packetBytes, - messageAuthenticator, - messageAuthenticatorPosition, - sharedSecret, - requestAuthenticator)) - { - throw new InvalidOperationException( - $"Invalid Message-Authenticator in packet {packet.Identifier}"); - } - } - - return packet; - } - - /// - /// Get the raw packet bytes - /// - /// - public byte[] GetBytes(IRadiusPacket packet, SharedSecret sharedSecret) - { - var packetBytes = new List - { - (byte)packet.Code, - packet.Identifier - }; - - packetBytes.AddRange(new byte[18]); // Placeholder for length and authenticator - - FillAttributes(packetBytes, packet.Authenticator, sharedSecret, packet.Attributes.Values, out int messageAuthenticatorPosition); - - // Note the order of the bytes... - var packetLengthBytes = BitConverter.GetBytes(packetBytes.Count); - packetBytes[2] = packetLengthBytes[1]; - packetBytes[3] = packetLengthBytes[0]; - - var packetBytesArray = packetBytes.ToArray(); - byte[] authenticator; - switch (packet.Code) - { - case PacketCode.AccountingRequest: - case PacketCode.DisconnectRequest: - case PacketCode.CoaRequest: - if (messageAuthenticatorPosition != 0) - { - FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret); - } - - authenticator = CalculateRequestAuthenticator(sharedSecret, packetBytesArray); - Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); - break; - case PacketCode.StatusServer: - authenticator = packet.RequestAuthenticator != null - ? CalculateResponseAuthenticator(sharedSecret, packet.RequestAuthenticator.Value, packetBytesArray) - : packet.Authenticator.Value; - Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); - - if (messageAuthenticatorPosition != 0) - { - FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret, packet.RequestAuthenticator); - } - break; - default: - if (packet.RequestAuthenticator == null) - { - Buffer.BlockCopy(packet.Authenticator.Value, 0, packetBytesArray, 4, 16); - } - - if (messageAuthenticatorPosition != 0) - { - FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret, packet.RequestAuthenticator); - } - - if (packet.RequestAuthenticator != null) - { - authenticator = CalculateResponseAuthenticator(sharedSecret, packet.RequestAuthenticator.Value, packetBytesArray); - Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); - } - break; - } - - return packetBytesArray; - } - - public RadiusPacket CreateResponsePacket(IRadiusPacket radiusPacket, PacketCode responsePacketCode) - { - if (radiusPacket is null) - throw new ArgumentNullException(nameof(radiusPacket)); - var header = RadiusPacketHeader.Create(responsePacketCode, radiusPacket.Identifier); - var packet = new RadiusPacket(header, requestAuthenticator: radiusPacket.Authenticator); - return packet; - } - - public bool TryGetNasIdentifier(byte[] packetBytes, out string nasIdentifier) - { - nasIdentifier = string.Empty; - - var packetLength = BitConverter.ToUInt16(packetBytes.Skip(2).Take(2).Reverse().ToArray(), 0); - if (packetBytes.Length != packetLength) - { - throw new InvalidOperationException($"Packet length does not match, expected: {packetLength}, actual: {packetBytes.Length}"); - } - - var position = 20; - while (position < packetBytes.Length) - { - var typecode = packetBytes[position]; - var length = packetBytes[position + 1]; - - if (position + length > packetLength) - { - throw new ArgumentOutOfRangeException("Invalid packet length"); - } - - if (typecode == NasIdentifierAttributeCode) - { - var contentBytes = new byte[length - 2]; - Buffer.BlockCopy(packetBytes, position + 2, contentBytes, 0, length - 2); - - nasIdentifier = Encoding.UTF8.GetString(contentBytes); - - return true; - } - - position += length; - } - - return false; - } - - private void FillMessageAuthenticator(byte[] packetBytesArray, int messageAuthenticatorPosition, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null) - { - var temp = new byte[16]; - Buffer.BlockCopy(temp, 0, packetBytesArray, messageAuthenticatorPosition + 2, 16); - var messageAuthenticatorBytes = CalculateMessageAuthenticator(packetBytesArray, sharedSecret, requestAuthenticator); - Buffer.BlockCopy(messageAuthenticatorBytes, 0, packetBytesArray, messageAuthenticatorPosition + 2, 16); - } - - private void FillAttributes(List packetBytes, RadiusAuthenticator authenticator, SharedSecret sharedSecret, IEnumerable attributes, out int messageAuthenticatorPosition) - { - messageAuthenticatorPosition = 0; - foreach (var attribute in attributes) - { - var attributeValues = attribute.Values; - foreach (var value in attributeValues) - { - var contentBytes = GetAttributeValueBytes(value); - var headerBytes = new byte[2]; - - var attributeType = _radiusDictionary.GetAttribute(attribute.Name); - switch (attributeType) - { - case DictionaryVendorAttribute vendorAttribute: - headerBytes = new byte[8]; - headerBytes[0] = RadiusAttributeCode.VendorSpecific; // VSA type - - var vendorId = BitConverter.GetBytes(vendorAttribute.VendorId); - Array.Reverse(vendorId); - Buffer.BlockCopy(vendorId, 0, headerBytes, 2, 4); - headerBytes[6] = (byte)vendorAttribute.VendorCode; - headerBytes[7] = (byte)(2 + contentBytes.Length); // length of the vsa part - break; - - case DictionaryAttribute dictionaryAttribute: - headerBytes[0] = attributeType.Code; - - // Encrypt password if this is a User-Password attribute - if (dictionaryAttribute.Code == RadiusAttributeCode.UserPassword) - { - contentBytes = RadiusPasswordProtector.Encrypt(sharedSecret, authenticator, contentBytes); - } - else if (dictionaryAttribute.Code == RadiusAttributeCode.MessageAuthenticator) // Remember the position of the message authenticator, because it has to be added after everything else - { - messageAuthenticatorPosition = packetBytes.Count; - } - - break; - default: - throw new InvalidOperationException( - "Unknown attribute {attribute.Key}, check spelling or dictionary"); - } - - headerBytes[1] = (byte)(headerBytes.Length + contentBytes.Length); - packetBytes.AddRange(headerBytes); - packetBytes.AddRange(contentBytes); - } - } - } - - /// - /// Gets the byte representation of an attribute object - /// - /// - /// - private static byte[] GetAttributeValueBytes(object value) - { - switch (value) - { - case string val: - return Encoding.UTF8.GetBytes(val); - - case uint val: - var contentBytes = BitConverter.GetBytes(val); - Array.Reverse(contentBytes); - return contentBytes; - - case int val: - contentBytes = BitConverter.GetBytes(val); - Array.Reverse(contentBytes); - return contentBytes; - - case byte[] val: - return val; - - case IPAddress val: - return val.GetAddressBytes(); - - default: - throw new NotImplementedException(); - } - } - - private AttributeValue? ParseVendorSpecificAttribute( - byte[] contentBytes, - byte typeCode, - RadiusAuthenticator authenticator, - SharedSecret sharedSecret) - { - var vsa = new VendorSpecificAttribute(contentBytes); - var vendorAttributeDefinition = _radiusDictionary.GetVendorAttribute(vsa.VendorId, vsa.VendorCode); - if (vendorAttributeDefinition == null) - { - _logger.LogDebug("Unknown vsa: {vendorId:l}:{vendorCode:l}", vsa.VendorId, vsa.VendorCode); - return null; - } - else - { - try - { - var content = ParseContentBytes( - vsa.Value, - vendorAttributeDefinition.Type, - typeCode, - authenticator, - sharedSecret); - - return new AttributeValue(vendorAttributeDefinition.Name, content); - } - catch (Exception ex) - { - _logger.LogError(ex, "Something went wrong with vsa {vsaName:l}", - vendorAttributeDefinition.Name); - return null; - } - } - } - - private AttributeValue? ParseAttribute( - byte[] contentBytes, - byte typeCode, - RadiusAuthenticator authenticator, - SharedSecret sharedSecret) - { - var attributeDefinition = _radiusDictionary.GetAttribute(typeCode); - - try - { - var content = ParseContentBytes( - contentBytes, - attributeDefinition.Type, - typeCode, - authenticator, - sharedSecret); - - return new AttributeValue( - attributeDefinition.Name, - content, - attributeDefinition.Code == RadiusAttributeCode.MessageAuthenticator); - } - catch (Exception ex) - { - _logger.LogError(ex, "Something went wrong with {attributeName:l}", attributeDefinition.Name); - _logger.LogDebug("Attribute bytes: {contentBytes}", contentBytes.ToHexString()); - return null; - } - } - - private static ushort GetPacketLength(byte[] packetBytes) - { - var packetLengthbytes = new byte[RadiusFieldOffsets.LengthFieldLength]; - // Length field always third and fourth bytes in packet (rfc2865) - packetLengthbytes[0] = packetBytes[RadiusFieldOffsets.LengthFieldPosition + 1]; - packetLengthbytes[1] = packetBytes[RadiusFieldOffsets.LengthFieldPosition]; - var packetLength = BitConverter.ToUInt16(packetLengthbytes, 0); - return packetLength; - } - - - private static byte[] CalculateRequestAuthenticator(SharedSecret sharedSecret, byte[] packetBytes) - { - return CalculateResponseAuthenticator(sharedSecret, new byte[16], packetBytes); - } - - private static byte[] CalculateResponseAuthenticator(SharedSecret sharedSecret, byte[] requestAuthenticator, byte[] packetBytes) - { - var responseAuthenticator = packetBytes.Concat(sharedSecret.Bytes).ToArray(); - Buffer.BlockCopy(requestAuthenticator, 0, responseAuthenticator, 4, 16); - - using var md5 = MD5.Create(); - return md5.ComputeHash(responseAuthenticator); - } - - private static byte[] CalculateMessageAuthenticator( - byte[] packetBytes, - SharedSecret sharedSecret, - RadiusAuthenticator? requestAuthenticator = null) - { - var temp = new byte[packetBytes.Length]; - packetBytes.CopyTo(temp, 0); - - requestAuthenticator?.Value.CopyTo(temp, 4); - - using var md5 = new HMACMD5(sharedSecret.Bytes); - return md5.ComputeHash(temp); - } - - private static object? ParseContentBytes( - byte[] contentBytes, - string type, - uint code, - RadiusAuthenticator authenticator, - SharedSecret sharedSecret) - { - switch (type) - { - case DictionaryAttribute.TypeTaggedString: - case DictionaryAttribute.TypeString: - //couse some NAS (like NPS) send binary within string attributes, check content before unpack to prevent data loss - if (contentBytes.All(b => b >= 32 && b <= 127)) //only if ascii - { - return Encoding.UTF8.GetString(contentBytes); - } - - return contentBytes; - - case DictionaryAttribute.TypeOctet: - // If this is a password attribute it must be decrypted - if (code == RadiusAttributeCode.UserPassword) - { - return RadiusPasswordProtector.Decrypt(sharedSecret, authenticator, contentBytes); - } - - return contentBytes; - - case DictionaryAttribute.TypeInteger: - case DictionaryAttribute.TypeTaggedInteger: - return BitConverter.ToInt32(contentBytes.Reverse().ToArray(), 0); - - case DictionaryAttribute.TypeIpAddr: - return new IPAddress(contentBytes); - - default: - return null; - } - } - - private bool IsMessageAuthenticatorValid( - byte[] packetBytes, - byte[] messageAuthenticator, - int messageAuthenticatorPosition, - SharedSecret sharedSecret, - RadiusAuthenticator requestAuthenticator) - { - var tempPacket = new byte[packetBytes.Length]; - packetBytes.CopyTo(tempPacket, 0); - - // Replace the Message-Authenticator content only. - // messageAuthenticatorPosition is a position of the Message-Authenticator block. - // The full-block length is 18: typecode (1), length (1), content (16). - // So the Message-Authenticator content position is (messageAuthenticatorPosition + 2). - Buffer.BlockCopy(new byte[16], 0, tempPacket, messageAuthenticatorPosition + 2, 16); - - var calculatedMessageAuthenticator = - CalculateMessageAuthenticator(tempPacket, sharedSecret, requestAuthenticator); - return calculatedMessageAuthenticator.SequenceEqual(messageAuthenticator); - } - - private record AttributeValue(string Name, object? Value, bool IsMessageAuthenticator = false); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusReplyAttributeService.cs deleted file mode 100644 index 5c03593f..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusReplyAttributeService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -namespace Multifactor.Radius.Adapter.v2.Services.Radius; - -public class RadiusReplyAttributeService : IRadiusReplyAttributeService -{ - private readonly IRadiusAttributeTypeConverter _converter; - private readonly ILogger _logger; - - public RadiusReplyAttributeService(IRadiusAttributeTypeConverter converter, ILogger logger) - { - _converter = converter; - _logger = logger; - } - - public IDictionary> GetReplyAttributes(GetReplyAttributesRequest request) - { - ArgumentNullException.ThrowIfNull(request); - - var attributes = new Dictionary>(); - foreach (var attr in request.ReplyAttributes) - { - var convertedValues = new List(); - - ProcessReplyAttributeValue(attr, request, convertedValues, out var breakLoop); - - attributes.Add(attr.Key, convertedValues); - if (breakLoop) - break; - } - - return attributes; - } - - private void ProcessReplyAttributeValue(KeyValuePair attr, GetReplyAttributesRequest request, List convertedValues, out bool breakLoop) - { - breakLoop = false; - foreach (var attrElement in attr.Value) - { - if (!IsMatch(request, attrElement)) - continue; - - foreach (var val in GetValues(request, attrElement)) - { - if (val is null) - { - _logger.LogDebug("Attribute '{attrname:l}' got no value, skipping", attr.Key); - continue; - } - - _logger.LogDebug("Added/replaced attribute '{attrname:l}:{attrval:l}' to reply", attr.Key, val.ToString()); - convertedValues.Add(_converter.ConvertType(attr.Key, val)); - } - - if (!attrElement.Sufficient) - continue; - - breakLoop = true; - return; - } - } - - private bool IsMatch(GetReplyAttributesRequest request, RadiusReplyAttributeValue attributeValue) - { - ArgumentNullException.ThrowIfNull(request); - - if (attributeValue.FromLdap) - { - if (attributeValue.IsMemberOf) - return request.UserGroups?.Count > 0; - - return request.HasAttribute(attributeValue.LdapAttributeName); - } - - if (attributeValue.UserNameCondition.Count != 0) - { - var userName = string.IsNullOrWhiteSpace(request.UserName) ? string.Empty : request.UserName; - var canonicalUserName = Utils.CanonicalizeUserName(userName); - return attributeValue.UserNameCondition.Any(x => CompareUserName(x, userName, canonicalUserName)); - } - - if (attributeValue.UserGroupCondition.Count != 0) - return attributeValue - .UserGroupCondition - .Intersect(request.UserGroups, StringComparer.OrdinalIgnoreCase) - .Any(); - - return true; - } - - private object?[] GetValues(GetReplyAttributesRequest context, RadiusReplyAttributeValue attributeValue) - { - if (attributeValue.IsMemberOf) - return context.UserGroups.Select(x => x as object).ToArray(); - - if (!attributeValue.FromLdap) - return [attributeValue.Value]; - - var attrValue = context.GetAttributeValues(attributeValue.LdapAttributeName); - return attrValue.Select(x => x as object).ToArray(); - } - - private bool CompareUserName(string conditionName, string userName, string canonicalUserName) - { - var toMatch = Utils.IsCanicalUserName(conditionName) - ? canonicalUserName - : userName; - - return string.Compare(toMatch, conditionName, StringComparison.InvariantCultureIgnoreCase) == 0; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/RandomWaiter.cs b/src/Multifactor.Radius.Adapter.v2/Services/RandomWaiter.cs deleted file mode 100644 index 4ca9e646..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Services/RandomWaiter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; - -namespace Multifactor.Radius.Adapter.v2.Services; - -public class RandomWaiter -{ - private readonly Random _random = new(); - private readonly RandomWaiterConfig _config; - - public RandomWaiter(RandomWaiterConfig config) - { - _config = config ?? throw new ArgumentNullException(nameof(config)); - } - - /// - /// Performs waiting task with configured delay values. - /// - /// Waiting task. - public Task WaitSomeTimeAsync() - { - if (_config.ZeroDelay) return Task.CompletedTask; - - var max = _config.Min == _config.Max ? _config.Max : _config.Max + 1; - var delay = _random.Next(_config.Min, max); - - return Task.Delay(TimeSpan.FromSeconds(delay)); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/content/radius.dictionary b/src/Multifactor.Radius.Adapter.v2/content/radius.dictionary index fc404c0c..afa38b33 100644 --- a/src/Multifactor.Radius.Adapter.v2/content/radius.dictionary +++ b/src/Multifactor.Radius.Adapter.v2/content/radius.dictionary @@ -1201,6 +1201,9 @@ VendorSpecificAttribute 2526 1 Ipass-Country-Code string VendorSpecificAttribute 2526 2 Ipass-Media-Access-Type string VendorSpecificAttribute 2526 3 Ipass-Location-Description string +# VendorId 2620 +VendorSpecificAttribute 2620 229 Gaia-User-Role string + # VendorId 2636 VendorSpecificAttribute 2636 1 Juniper-Local-User-Name string VendorSpecificAttribute 2636 2 Juniper-Allow-Commands string @@ -1340,6 +1343,7 @@ VendorSpecificAttribute 3076 66 Altiga-IPSec-Authorization-Required-G integer VendorSpecificAttribute 3076 67 Altiga-IPSec-DN-Field-G string VendorSpecificAttribute 3076 68 Altiga-IPSec-Confidence-Level-G integer VendorSpecificAttribute 3076 75 Altiga-LEAP-Bypass-G integer +VendorSpecificAttribute 3076 85 Tunnel-Group-Lock string VendorSpecificAttribute 3076 128 Altiga-Part-Primary-DHCP-G ipaddr VendorSpecificAttribute 3076 129 Altiga-Part-Secondary-DHCP-G ipaddr VendorSpecificAttribute 3076 131 Altiga-Part-Premise-Router-G ipaddr @@ -1392,6 +1396,7 @@ VendorSpecificAttribute 3414 41 Ipass-3414-41 string VendorSpecificAttribute 3414 42 Ipass-3414-42 string VendorSpecificAttribute 3414 43 Ipass-3414-43 string + # VendorId 3551 VendorSpecificAttribute 3551 1 ST-Acct-VC-Connection-Id string VendorSpecificAttribute 3551 2 ST-Service-Name string @@ -1822,6 +1827,11 @@ VendorSpecificAttribute 25461 8 PaloAlto-PaloAlto-Client-OS string VendorSpecificAttribute 25461 9 PaloAlto-Client-Hostname string VendorSpecificAttribute 25461 10 PaloAlto-GlobalProtect-Version string + +# VendorId 39410 +VendorSpecificAttribute 39410 30 Ideco-Administrator-Role string +VendorSpecificAttribute 39410 31 Ideco-Administrator-Name string + # VendorId 774641 (Multifactor) VendorSpecificAttribute 774641 1 mfa-client-name string VendorSpecificAttribute 774641 2 mfa-client-ver string \ No newline at end of file diff --git a/src/multifactor-radius-adapter.sln b/src/multifactor-radius-adapter.sln index 9ddfb680..3b36bd2c 100644 --- a/src/multifactor-radius-adapter.sln +++ b/src/multifactor-radius-adapter.sln @@ -9,17 +9,18 @@ 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 + compose.yaml = compose.yaml 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2", "Multifactor.Radius.Adapter.v2\Multifactor.Radius.Adapter.v2.csproj", "{03C86912-C263-4CF9-A0D8-94167AB647BE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2.Tests", "Multifactor.Radius.Adapter.v2.Tests\Multifactor.Radius.Adapter.v2.Tests.csproj", "{3E7169B6-274B-48F0-A56F-9C227DDD596A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2.EndToEndTests", "Multifactor.Radius.Adapter.v2.EndToEndTests\Multifactor.Radius.Adapter.v2.EndToEndTests.csproj", "{3882D448-6BDC-449C-AC9E-687EE82F407B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2.Infrastructure", "Multifactor.Radius.Adapter.v2.Infrastructure\Multifactor.Radius.Adapter.v2.Infrastructure.csproj", "{1C003665-1D1D-428F-ACB5-F4489BC21C04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2.Application", "Multifactor.Radius.Adapter.v2.Application\Multifactor.Radius.Adapter.v2.Application.csproj", "{3B698212-A44A-49BF-BA2C-B2FF1FC9780F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter.v2.Shared", "Multifactor.Radius.Adapter.v2.Shared\Multifactor.Radius.Adapter.v2.Shared.csproj", "{7B428E7E-778B-4F37-8911-EB0B893B1AFE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,14 +28,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E8A7518C-A622-4343-A594-46EE5869EE96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8A7518C-A622-4343-A594-46EE5869EE96}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8A7518C-A622-4343-A594-46EE5869EE96}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8A7518C-A622-4343-A594-46EE5869EE96}.Release|Any CPU.Build.0 = Release|Any CPU - {8C663BCC-03FE-437C-A81E-1B581BF2BD3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 {03C86912-C263-4CF9-A0D8-94167AB647BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03C86912-C263-4CF9-A0D8-94167AB647BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {03C86912-C263-4CF9-A0D8-94167AB647BE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -43,10 +36,18 @@ Global {3E7169B6-274B-48F0-A56F-9C227DDD596A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E7169B6-274B-48F0-A56F-9C227DDD596A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E7169B6-274B-48F0-A56F-9C227DDD596A}.Release|Any CPU.Build.0 = Release|Any CPU - {3882D448-6BDC-449C-AC9E-687EE82F407B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3882D448-6BDC-449C-AC9E-687EE82F407B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3882D448-6BDC-449C-AC9E-687EE82F407B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3882D448-6BDC-449C-AC9E-687EE82F407B}.Release|Any CPU.Build.0 = Release|Any CPU + {1C003665-1D1D-428F-ACB5-F4489BC21C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C003665-1D1D-428F-ACB5-F4489BC21C04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C003665-1D1D-428F-ACB5-F4489BC21C04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C003665-1D1D-428F-ACB5-F4489BC21C04}.Release|Any CPU.Build.0 = Release|Any CPU + {3B698212-A44A-49BF-BA2C-B2FF1FC9780F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B698212-A44A-49BF-BA2C-B2FF1FC9780F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B698212-A44A-49BF-BA2C-B2FF1FC9780F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B698212-A44A-49BF-BA2C-B2FF1FC9780F}.Release|Any CPU.Build.0 = Release|Any CPU + {7B428E7E-778B-4F37-8911-EB0B893B1AFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B428E7E-778B-4F37-8911-EB0B893B1AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B428E7E-778B-4F37-8911-EB0B893B1AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B428E7E-778B-4F37-8911-EB0B893B1AFE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE