From d898a5c3e56f9793348329259bc3ea5915fc5fd8 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 14 Jan 2026 13:46:27 +0300 Subject: [PATCH 01/11] First refactoring iteration --- .../Attributes/DictionaryVendorAttribute.cs | 48 -- .../Cache/IAuthenticatedClientCache.cs | 9 + .../Cache/ICacheService.cs | 3 +- .../Models}/ApplicationVariables.cs | 2 +- .../Models/ClientConfiguration.cs | 36 ++ .../Models/LdapServerConfiguration.cs | 26 + .../Models/RadiusReplyAttribute.cs | 12 + .../Configuration/Models/RootConfiguration.cs | 24 + .../Models/ServiceConfiguration.cs | 11 + .../Extensions/ApplicationExtensions.cs | 74 +++ .../ChallengeProcessorProvider.cs | 4 +- .../ChangePasswordChallengeProcessor.cs | 72 +-- .../AccessChallenge/IChallengeProcessor.cs | 13 + .../IChallengeProcessorProvider.cs | 4 +- .../Models}/ChallengeIdentifier.cs | 2 +- .../AccessChallenge/Models/ChallengeStatus.cs | 8 + .../AccessChallenge/Models/ChallengeType.cs | 8 + .../Models/PasswordChangeCache.cs} | 9 +- .../AccessChallenge/Models}/PersonalData.cs | 2 +- .../SecondFactorChallengeProcessor.cs | 110 ++-- .../ActiveDirectoryFormatter.cs | 5 +- .../BindNameFormat/FreeIpaFormatter.cs | 8 +- .../BindNameFormat/ILdapBindNameFormatter.cs | 11 + .../ILdapBindNameFormatterProvider.cs | 2 +- .../LdapBindNameFormatterProvider.cs | 4 +- .../BindNameFormat/MultiDirectoryFormatter.cs | 8 +- .../BindNameFormat/OpenLdapFormatter.cs | 8 +- .../BindNameFormat/SambaFormatter.cs | 8 +- .../FirstFactorProcessorProvider.cs | 4 +- .../FirstFactor/IFirstFactorProcessor.cs | 11 + .../IFirstFactorProcessorProvider.cs | 8 + .../FirstFactor/LdapFirstFactorProcessor.cs | 181 +++++++ .../FirstFactor/NoneFirstFactorProcessor.cs | 12 +- .../FirstFactor/RadiusFirstFactorProcessor.cs | 39 +- .../Features/Ldap/ILdapAdapter.cs | 17 + .../Ldap/Models/ChangeUserPasswordRequest.cs | 15 + .../Ldap/Models/CheckConnectionRequest.cs | 9 + .../Features/Ldap/Models/FindUserRequest.cs | 18 + .../Features/Ldap/Models}/ILdapProfile.cs | 3 +- .../Features/Ldap/Models}/LdapProfile.cs | 2 +- .../Features/Ldap/Models/LoadSchemaRequest.cs | 22 + .../Ldap/Models/LoadUserGroupRequest.cs | 16 + .../Features/Ldap/Models/MembershipRequest.cs | 36 ++ .../MultifactorApiUnreachableException.cs | 4 +- .../Multifactor/Models/AccessRequestQuery.cs} | 8 +- .../Models}/AccessRequestResponse.cs | 4 +- .../Models/ChallengeRequestQuery.cs | 8 + .../Models/SecondFactorResponse.cs | 15 + .../Multifactor/MultifactorApiService.cs | 373 +++++++++++++ .../Multifactor/Ports/IMultifactorApi.cs | 9 + .../Features/Pipeline/IPipelineProvider.cs | 6 + .../Features/Pipeline/IRadiusPipeline.cs | 8 + .../Pipeline/IRadiusPipelineFactory.cs | 8 + .../Models/Enum}/UserIdentityFormat.cs | 2 +- .../Pipeline/Models/RadiusPipelineContext.cs | 43 ++ .../Pipeline/Models/ResponseInformation.cs | 7 + .../Features/Pipeline/Models}/UserIdentity.cs | 14 +- .../Pipeline/Models}/UserPassphrase.cs | 30 +- .../Pipeline/Steps/AccessChallengeStep.cs | 13 +- .../Steps/AccessGroupsCheckingStep.cs | 82 +++ .../Steps/AccessRequestFilteringStep.cs | 44 ++ .../Pipeline/Steps/FirstFactorStep.cs | 25 +- .../Pipeline/Steps/IRadiusPipelineStep.cs | 8 + .../Pipeline/Steps/IpWhiteListStep.cs | 18 +- .../Pipeline/Steps/LdapSchemaLoadingStep.cs | 67 +++ .../Pipeline/Steps/PreAuthCheckStep.cs | 21 +- .../Pipeline/Steps/PreAuthPostCheck.cs | 12 +- .../Pipeline/Steps/ProfileLoadingStep.cs | 58 ++- .../Pipeline/Steps/SecondFactorStep.cs | 82 +-- .../Steps/StatusServerFilteringStep.cs | 19 +- .../Pipeline/Steps/UserGroupLoadingStep.cs | 150 ++++++ .../Pipeline/Steps/UserNameValidationStep.cs | 41 +- .../Exceptions/PipelineNotFoundException.cs | 18 + .../Exceptions/RadiusPacketException.cs | 9 + .../Exceptions/RadiusProcessingException.cs | 23 + .../Radius/Models/Enums}/AccountType.cs | 2 +- .../Models/Enums}/AuthenticationType.cs | 2 +- .../Radius/Models/Enums}/PacketCode.cs | 2 +- .../Models/GetReplyAttributesRequest.cs | 41 ++ .../Radius/Models}/IResponseInformation.cs | 2 +- .../Features/Radius/Models/ParsedAttribute.cs | 15 + .../Radius/Models}/RadiusAttribute.cs | 2 +- .../Radius/Models}/RadiusAuthenticator.cs | 12 +- .../Features/Radius/Models}/RadiusPacket.cs | 18 +- .../Radius/Models}/RadiusPacketHeader.cs | 18 +- .../Models/SendAdapterResponseRequest.cs | 47 ++ .../Features/Radius/Models}/SharedSecret.cs | 3 +- .../Features/Radius/Ports}/IRadiusClient.cs | 2 +- .../Radius/Ports}/IRadiusClientFactory.cs | 2 +- .../Radius/Ports/IRadiusPacketService.cs | 13 + .../Radius/Ports/IRadiusUdpAdapter.cs | 8 + .../Features/Radius/Ports/IResponseSender.cs | 8 + .../Services/IRadiusAttributeTypeConverter.cs | 6 + .../Radius/Services/IRadiusPacketProcessor.cs | 9 + .../Services/IRadiusReplyAttributeService.cs | 8 + .../Radius/Services/RadiusPacketProcessor.cs | 179 +++++++ .../Models/Enum}/AuthenticationSource.cs | 2 +- .../Models/Enum}/AuthenticationStatus.cs | 2 +- .../Models/Enum}/PreAuthMode.cs | 2 +- .../Models/Enum}/PrivacyMode.cs | 2 +- .../Models/Enum}/RequestStatus.cs | 2 +- ...actor.Radius.Adapter.v2.Application.csproj | 20 + .../Ports/IUdpClient.cs | 11 + .../Security/ProtectionService.cs | 50 ++ .../Security}/RadiusPasswordProtector.cs | 5 +- .../E2ETestBase.cs | 14 +- .../E2ETestsUtils.cs | 6 +- .../Fixtures/RadiusPacketFactory.cs | 2 +- .../RadiusFixtures.cs | 3 +- .../Tests/AccessChallengeTests.cs | 5 +- .../Tests/AccessRequestAttributesTests.cs | 4 +- .../Tests/BypassWhenApiUnreachableTests.cs | 5 +- .../Tests/ChangePasswordTests.cs | 4 +- .../Tests/FirstFactorTests.cs | 5 +- .../MultipleActiveDirectory2FaGroupsTests.cs | 5 +- .../MultipleActiveDirectoryGroupsTests.cs | 5 +- .../Tests/PreSecondFactorTests.cs | 5 +- .../Tests/ReplyAttributesTests.cs | 5 +- ...ingleActiveDirectory2FaBypassGroupTests.cs | 5 +- .../SingleActiveDirectory2FaGroupTests.cs | 5 +- .../Tests/SingleActiveDirectoryGroupTests.cs | 5 +- .../Ldap/CustomLdapConnectionFactory.cs | 7 +- .../Adapters/Ldap/LdapAdapter.cs | 162 ++++++ .../Adapters}/Ldap/LdapConnection.cs | 7 +- .../Multifactor/Models/AccessRequestDto.cs | 41 ++ .../Multifactor/Models/ChallengeRequestDto.cs | 21 + .../Models/MultifactorApiResponse.cs | 7 + .../Adapters/Multifactor/MultifactorApi.cs | 63 +++ .../PacketHandler/RadiusUdpAdapter.cs} | 50 +- .../Adapters/Udp/CustomUdpClient.cs | 133 +++++ .../AuthenticatedClient.cs | 2 +- .../AuthenticatedClientCache.cs | 21 +- .../Cache/CacheService.cs | 11 +- .../Attributes/DictionaryAttribute.cs | 2 +- .../Attributes/DictionaryVendorAttribute.cs | 2 +- .../Attributes/VendorSpecificAttribute.cs | 4 +- .../Dictionary}/IRadiusDictionary.cs | 4 +- .../Dictionary/RadiusDictionary.cs | 140 +++++ .../InvalidConfigurationException.cs | 66 ++- .../Loader/ConfigurationLoader.cs | 84 +++ .../Loader/IConfigurationLoader.cs | 8 + .../Parser/IConfigurationParser.cs | 9 + .../Parser/ValueParser/IValueParser.cs | 25 + .../Parser/ValueParser/ValueParser.cs | 262 ++++++++++ .../Parser/XmlConfigurationParser.cs | 214 ++++++++ .../Configurations/Reader/IXmlReader.cs | 11 + .../Configurations/Reader/XmlReader.cs | 64 +++ .../Extensions/InfrastructureExtensions.cs | 169 ++++++ .../Http/ActivityContext.cs | 45 ++ .../Http/BasicAuthHeaderValue.cs | 5 +- .../Http/MfTraceIdHeaderSetter.cs | 23 + .../Http/RoundRobinEndpointSelector.cs | 56 ++ .../Http/WebProxyFactory.cs | 10 +- .../Logging/CustomCompactJsonFormatter.cs | 0 .../Logging/SerilogJsonFormatterTypes.cs | 0 .../Logging/SerilogLoggerFactory.cs | 56 +- .../Logging/StartupLogger.cs | 5 +- ...or.Radius.Adapter.v2.Infrastructure.csproj | 24 + .../Pipeline/RadiusPipeline.cs | 35 ++ .../Pipeline/RadiusPipelineFactory.cs | 80 +++ .../Pipeline/RadiusPipelineProvider.cs | 52 ++ .../Radius/Builders/IRadiusPacketBuilder.cs | 10 + .../Builders/RadiusAttributeSerializer.cs | 194 +++++++ .../Radius/Builders/RadiusPacketBuilder.cs | 172 ++++++ .../Radius/Client}/RadiusClient.cs | 6 +- .../Radius/Client}/RadiusClientFactory.cs | 3 +- .../Radius/Crypto/IRadiusCryptoProvider.cs | 13 + .../Radius/Crypto/RadiusCryptoProvider.cs | 86 +++ .../Radius/Crypto/RadiusPasswordProtector.cs | 58 +++ .../Radius/Parsers/IRadiusAttributeParser.cs | 8 + .../Radius/Parsers/IRadiusPacketParser.cs | 9 + .../Radius/Parsers/RadiusAttributeParser.cs | 220 ++++++++ .../Radius/Parsers/RadiusPacketParser.cs | 157 ++++++ .../Radius/Sender/AdapterResponseSender.cs | 307 +++++++++++ .../Services/RadiusAttributeTypeConverter.cs | 152 ++++++ .../Radius/Services/RadiusPacketService.cs | 122 +++++ .../Services/RadiusReplyAttributeService.cs | 195 +++++++ .../Validators/IRadiusPacketValidator.cs | 10 + .../Validators/RadiusPacketValidator.cs | 136 +++++ .../BytesExtensions.cs | 9 + .../HttpRequestMessageExtension.cs | 47 ++ ...ultifactor.Radius.Adapter.v2.Shared.csproj | 9 + .../StringExtension.cs | 69 +++ .../ChallengeProcessorProviderTests.cs | 3 +- .../ChangePasswordChallengeProcessorTests.cs | 12 +- .../SecondFactorChallengeProcessorTests.cs | 15 +- .../AppSettingsTests.cs | 2 - .../LdapServerConfigurationTests.cs | 1 - .../ServiceConfigurationFactoryTests.cs | 1 - .../CustomLdapSchemaLoaderTests.cs | 1 - .../LdapFirstFactorProcessorTests.cs | 11 +- .../RadiusFirstFactorProcessorTests.cs | 10 +- .../Fixture/TestUtils.cs | 3 +- .../LdapPasswordChangerTests.cs | 3 +- .../LdapProfile/LdapProfileLoaderTests.cs | 2 - .../LdapProfile/LdapProfileServiceTests.cs | 5 +- .../LdapProfile/LdapProfileTest.cs | 16 +- .../PipelineTests/BuildPipelineTests.cs | 1 - .../PipelineTests/PerformanceTests.cs | 3 +- .../PipelineConfigurationFactoryTests.cs | 7 +- .../PipelineTests/PipelineExecutionTests.cs | 4 +- .../AccessGroupsCheckingStepTests.cs | 9 +- .../AccessRequestFilteringStepTests.cs | 6 +- .../StepsTests/PreAuthCheckStepTests.cs | 7 +- .../StepsTests/PreAuthPostCheckStepTests.cs | 5 +- .../StepsTests/ProfileLoadingStepTests.cs | 6 +- .../StepsTests/SecondFactorStepTests.cs | 18 +- .../StatusServerFilteringStepTests.cs | 10 +- .../StepsTests/UserGroupLoadingStepTests.cs | 7 +- .../Radius/NasIdentifierParserTests.cs | 3 +- .../PacketService/AttributeReadingTests.cs | 3 +- .../PacketService/RadiusPacketParsingTests.cs | 4 +- .../PacketService/ResponsePacketTests.cs | 4 +- .../Radius/RadiusAttributeTests.cs | 1 - .../Radius/RadiusPacketTests.cs | 2 +- .../Server/UdpPacketHandlerTests.cs | 11 +- .../AddPipelineTests.cs | 7 +- .../Unit/AdapterResponseSenderTests.cs | 13 +- .../LdapFirstFactorProcessorTests.cs | 10 +- .../RadiusFirstFactorProcessorTests.cs | 12 +- .../ActiveDirectoryTests.cs | 4 +- .../FreeIpaTests.cs | 4 +- .../LdapBindNameFormatterProviderTests.cs | 2 +- .../MultiDirectoryTests.cs | 4 +- .../OpenLdapTests.cs | 4 +- .../LdapBindNameFormatterTests/SambaTests.cs | 4 +- .../Unit/LdapForestServiceTests.cs | 338 ------------ .../Unit/LdapGroupServiceTests.cs | 4 +- .../MultifactorApiServiceTests.cs | 10 +- .../MultifactorApi/MultifactorApiTests.cs | 1 - .../PipelineSteps/IpWhiteListStepTests.cs | 6 +- .../StepsTests/UserNameValidationStepTests.cs | 6 +- .../Unit/RadiusAttributeTypeConverterTests.cs | 2 +- .../Unit/RadiusReplyAttributeServiceTests.cs | 3 +- .../UserIdentityTests/UserIdentityTests.cs | 4 +- .../Core/AccessChallenge/ChallengeStatus.cs | 8 - .../Core/AccessChallenge/ChallengeType.cs | 8 - .../AccessChallenge/IChallengeProcessor.cs | 12 - .../Auth/AuthenticatedClientCacheConfig.cs | 24 - .../Core/Auth/AuthenticationState.cs | 7 - .../Core/Auth/IAuthenticationState.cs | 8 - .../Auth/PreAuthMode/PreAuthModeDescriptor.cs | 41 -- .../Auth/PreAuthMode/PreAuthModeSettings.cs | 19 - .../Core/Auth/UserNameTransformRules.cs | 28 - .../Build/ClientConfigurationFactory.cs | 483 ----------------- .../DefaultClientConfigurationsProvider.cs | 91 ---- .../Build/IClientConfigurationFactory.cs | 9 - .../Build/IClientConfigurationsProvider.cs | 27 - .../Client/ClientConfiguration.cs | 221 -------- .../Client/IClientConfiguration.cs | 32 -- .../Client/ILdapServerConfiguration.cs | 28 - .../Configuration/Client/IPermissionRules.cs | 8 - .../Client/LdapServerConfiguration.cs | 205 -------- .../Client/LdapServerInitializeRequest.cs | 68 --- .../Configuration/Client/PermissionRules.cs | 44 -- .../Client/RadiusReplyAttributeValue.cs | 81 --- .../Build/IServiceConfigurationFactory.cs | 8 - .../Build/ServiceConfigurationFactory.cs | 201 ------- .../Service/IServiceConfiguration.cs | 19 - .../Service/ServiceConfiguration.cs | 147 ------ .../BindNameFormat/ILdapBindNameFormatter.cs | 10 - .../Core/FirstFactor/IFirstFactorProcessor.cs | 11 - .../IFirstFactorProcessorProvider.cs | 8 - .../FirstFactor/LdapFirstFactorProcessor.cs | 166 ------ .../Core/Ldap/Forest/ForestFilter.cs | 26 - .../Core/Ldap/Forest/IForestFilter.cs | 10 - .../Core/Ldap/Forest/LdapForestEntry.cs | 47 -- .../Core/Ldap/ILdapConnection.cs | 17 - .../Core/Ldap/ILdapConnectionFactory.cs | 8 - .../Ldap/LdapConnectionStringExtensions.cs | 17 - .../Core/Ldap/LdapErrorReasonInfo.cs | 118 ----- .../Core/Ldap/LdapNamesUtils.cs | 31 -- .../Core/MultifactorApi/ApiCredential.cs | 24 - .../Core/MultifactorApi/Capabilities.cs | 6 - .../Core/MultifactorApi/ChallengeRequest.cs | 8 - .../Core/MultifactorApi/GroupPolicyPreset.cs | 6 - .../MultifactorApi/MultifactorApiResponse.cs | 7 - .../MultifactorApi/MultifactorResponse.cs | 18 - .../PrivacyMode/PrivacyModeDescriptor.cs | 72 --- .../Core/Pipeline/ExecutionState.cs | 20 - .../Core/Pipeline/IExecutionState.cs | 11 - .../Core/Pipeline/IResponseSender.cs | 8 - .../Core/Pipeline/ResponseInformation.cs | 8 - .../Settings/IPipelineExecutionSettings.cs | 33 -- .../Settings/PipelineExecutionSettings.cs | 45 -- .../Radius/Attributes/RadiusDictionary.cs | 101 ---- .../Attributes/VendorSpecificAttribute.cs | 62 --- .../Radius/Metadata/RadiusAttributeCode.cs | 20 - .../Radius/Metadata/RadiusFieldOffsets.cs | 16 - .../Core/Radius/Packet/IRadiusPacket.cs | 36 -- .../Core/Radius/SharedSecret.cs | 37 -- .../RandomWaiterFeature/RandomWaiterConfig.cs | 43 -- .../Core/UserNameTransformation.cs | 32 -- .../Core/UserPassphrase.cs | 106 ---- .../Core/Utils.cs | 110 ---- .../Exceptions/LdapUserNotFoundException.cs | 11 - .../MultifactorApiUnreachableException.cs | 12 - .../Extensions/ServiceCollectionExtensions.cs | 261 ---------- .../Extensions/ToPascalCaseExtension.cs | 22 - .../ConfigurationBuilderExtensions.cs | 34 -- .../Configuration/ConfigurationExtensions.cs | 25 - .../Configuration/IPEndPointFactory.cs | 34 -- .../RadiusAdapterConfigurationFactory.cs | 96 ---- .../RadiusAdapterConfigurationProvider.cs | 27 - .../RadiusAdapterConfiguration.cs | 33 -- .../RadiusAdapterConfigurationDescription.cs | 51 -- .../RadiusAdapterConfigurationFile.cs | 21 - .../Sections/AppSettingsSection.cs | 98 ---- .../LdapServer/LdapServerConfiguration.cs | 66 --- .../Sections/LdapServer/LdapServersSection.cs | 35 -- .../RadiusReply/RadiusReplyAttribute.cs | 14 - .../RadiusReplyAttributesSection.cs | 48 -- .../RadiusReply/RadiusReplySection.cs | 10 - .../UserNameTransformRule.cs | 8 - .../UserNameTransformRulesCollection.cs | 34 -- .../UserNameTransformRulesSection.cs | 11 - .../UserNameTransformSettings.cs | 17 - .../RadiusConfigurationEnvironmentVariable.cs | 18 - .../RadiusConfigurationFile.cs | 73 --- .../RadiusConfigurationSource.cs | 39 -- .../XmlAppConfigurationSource.cs | 122 ----- .../XmlAppConfiguration/XmlAssert.cs | 73 --- .../Http/MultifactorHttpClient.cs | 86 --- .../Pipeline/Builder/IPipelineBuilder.cs | 9 - .../Pipeline/Builder/PipelineBuilder.cs | 31 -- .../IPipelineConfigurationFactory.cs | 8 - .../IPipelineStepsConfiguration.cs | 11 - .../Configuration/PipelineConfiguration.cs | 13 - .../PipelineConfigurationFactory.cs | 98 ---- .../PipelineStepsConfiguration.cs | 26 - .../IRadiusPipelineExecutionContext.cs | 51 -- .../Context/RadiusPipelineExecutionContext.cs | 64 --- .../Pipeline/IPipelineProvider.cs | 6 - .../Pipeline/IRadiusPipeline.cs | 8 - .../Pipeline/PipelineProvider.cs | 83 --- .../Infrastructure/Pipeline/RadiusPipeline.cs | 28 - .../Steps/AccessGroupsCheckingStep.cs | 85 --- .../Steps/AccessRequestFilteringStep.cs | 29 -- .../Pipeline/Steps/IRadiusPipelineStep.cs | 8 - .../Pipeline/Steps/LdapSchemaLoadingStep.cs | 80 --- .../Pipeline/Steps/UserGroupLoadingStep.cs | 151 ------ .../Multifactor.Radius.Adapter.v2.csproj | 9 +- src/Multifactor.Radius.Adapter.v2/Program.cs | 36 +- .../Server/AdapterServer.cs | 218 +++++--- .../Server/IRadiusPacketProcessor.cs | 9 - .../Server/RadiusPacketProcessor.cs | 127 ----- .../Server/ServerHost.cs | 45 +- .../Server/Udp/CustomUdpClient.cs | 32 -- .../Server/Udp/IUdpClient.cs | 10 - .../Server/Udp/IUdpPacketHandler.cs | 8 - .../AdapterResponseSender.cs | 164 ------ .../SendAdapterResponseRequest.cs | 52 -- .../IAuthenticatedClientCache.cs | 9 - .../DataProtection/IDataProtectionService.cs | 8 - .../DataProtection/LinuxProtectionService.cs | 42 -- .../WindowsProtectionService.cs | 45 -- .../Ldap/ChangeUserPasswordRequest.cs | 26 - .../Services/Ldap/CustomLdapSchemaLoader.cs | 44 -- .../Services/Ldap/FindUserProfileRequest.cs | 33 -- .../Forest/ActiveDirectoryLdapForestLoader.cs | 56 -- .../Services/Ldap/Forest/ILdapForestLoader.cs | 14 - .../Ldap/Forest/ILdapForestLoaderProvider.cs | 8 - .../Ldap/Forest/ILdapForestService.cs | 9 - .../Forest/ILdapServerConfigurationService.cs | 9 - .../Ldap/Forest/LdapForestLoaderProvider.cs | 18 - .../Services/Ldap/Forest/LdapForestService.cs | 147 ------ .../Forest/LdapServerConfigurationService.cs | 25 - .../Services/Ldap/ILdapGroupService.cs | 8 - .../Services/Ldap/ILdapPasswordChanger.cs | 8 - .../Services/Ldap/ILdapProfileLoader.cs | 13 - .../Services/Ldap/ILdapProfileService.cs | 9 - .../Services/Ldap/ILdapSchemaLoader.cs | 9 - .../Services/Ldap/ILdapSchemeLoaderWrapper.cs | 9 - .../Services/Ldap/LdapGroupService.cs | 84 --- .../Services/Ldap/LdapPasswordChanger.cs | 62 --- .../Services/Ldap/LdapProfileLoader.cs | 36 -- .../Services/Ldap/LdapProfileService.cs | 98 ---- .../Services/Ldap/LdapSchemaLoaderWrapper.cs | 24 - .../Services/Ldap/LoadUserGroupsRequest.cs | 28 - .../Services/Ldap/MembershipRequest.cs | 39 -- .../Services/Ldap/PasswordChangeRequest.cs | 12 - .../Services/Ldap/PasswordChangeResponse.cs | 8 - .../CreateSecondFactorRequest.cs | 64 --- .../MultifactorApi/IMultifactorApi.cs | 9 - .../MultifactorApi/IMultifactorApiService.cs | 9 - .../Services/MultifactorApi/MultifactorApi.cs | 107 ---- .../MultifactorApi/MultifactorApiService.cs | 395 -------------- .../MultifactorApi/SendChallengeRequest.cs | 49 -- .../Radius/GetReplyAttributesRequest.cs | 56 -- .../Radius/IRadiusAttributeTypeConverter.cs | 6 - .../Services/Radius/IRadiusPacketService.cs | 12 - .../Radius/IRadiusReplyAttributeService.cs | 6 - .../Radius/RadiusAttributeTypeConverter.cs | 66 --- .../Radius/RadiusPacketNasIdentifierParser.cs | 48 -- .../Services/Radius/RadiusPacketService.cs | 492 ------------------ .../Radius/RadiusReplyAttributeService.cs | 113 ---- .../Services/RandomWaiter.cs | 28 - .../content/radius.dictionary | 10 + src/multifactor-radius-adapter.sln | 18 + 399 files changed, 6457 insertions(+), 9311 deletions(-) delete mode 100644 src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs rename src/{Multifactor.Radius.Adapter.v2/Services => Multifactor.Radius.Adapter.v2.Application}/Cache/ICacheService.cs (66%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Configuration/Models}/ApplicationVariables.cs (77%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/AccessChallenge/ChallengeProcessorProvider.cs (82%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/AccessChallenge/ChangePasswordChallengeProcessor.cs (56%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/AccessChallenge/IChallengeProcessorProvider.cs (57%) rename src/{Multifactor.Radius.Adapter.v2/Core/AccessChallenge => Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models}/ChallengeIdentifier.cs (90%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs rename src/{MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs => Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs} (66%) rename src/{Multifactor.Radius.Adapter.v2/Core/MultifactorApi => Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models}/PersonalData.cs (77%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/AccessChallenge/SecondFactorChallengeProcessor.cs (60%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs (58%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/FreeIpaFormatter.cs (59%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs (67%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs (85%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs (60%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/OpenLdapFormatter.cs (59%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/BindNameFormat/SambaFormatter.cs (59%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/FirstFactorProcessorProvider.cs (85%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/NoneFirstFactorProcessor.cs (55%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Application/Features}/FirstFactor/RadiusFirstFactorProcessor.cs (70%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Ldap => Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models}/ILdapProfile.cs (82%) rename src/{Multifactor.Radius.Adapter.v2/Core/Ldap => Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models}/LdapProfile.cs (95%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs rename src/{MultiFactor.Radius.Adapter/Services/MultiFactorApi => Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions}/MultifactorApiUnreachableException.cs (89%) rename src/{Multifactor.Radius.Adapter.v2/Core/MultifactorApi/AccessRequest.cs => Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs} (60%) rename src/{Multifactor.Radius.Adapter.v2/Core/MultifactorApi => Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models}/AccessRequestResponse.cs (84%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/ChallengeRequestQuery.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Ldap/Identity => Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum}/UserIdentityFormat.cs (68%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/ResponseInformation.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Ldap/Identity => Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models}/UserIdentity.cs (66%) rename src/{MultiFactor.Radius.Adapter/Core/Framework/Context => Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models}/UserPassphrase.cs (77%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/AccessChallengeStep.cs (74%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessRequestFilteringStep.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/FirstFactorStep.cs (60%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IRadiusPipelineStep.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/IpWhiteListStep.cs (65%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/PreAuthCheckStep.cs (61%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/PreAuthPostCheck.cs (65%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/ProfileLoadingStep.cs (60%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/SecondFactorStep.cs (55%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/StatusServerFilteringStep.cs (59%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/Steps/UserNameValidationStep.cs (51%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusPacketException.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums}/AccountType.cs (53%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums}/AuthenticationType.cs (63%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/Enums}/PacketCode.cs (85%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Pipeline => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/IResponseInformation.cs (62%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/ParsedAttribute.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/RadiusAttribute.cs (89%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/RadiusAuthenticator.cs (69%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/RadiusPacket.cs (93%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Packet => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/RadiusPacketHeader.cs (70%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs rename src/{MultiFactor.Radius.Adapter/Core/Radius => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models}/SharedSecret.cs (96%) rename src/{Multifactor.Radius.Adapter.v2/Services/Radius => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports}/IRadiusClient.cs (71%) rename src/{Multifactor.Radius.Adapter.v2/Services/Radius => Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports}/IRadiusClientFactory.cs (62%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusPacketService.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IRadiusUdpAdapter.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IResponseSender.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusAttributeTypeConverter.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusReplyAttributeService.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs rename src/{Multifactor.Radius.Adapter.v2/Core/Auth => Multifactor.Radius.Adapter.v2.Application/Models/Enum}/AuthenticationSource.cs (64%) rename src/{Multifactor.Radius.Adapter.v2/Core/Auth => Multifactor.Radius.Adapter.v2.Application/Models/Enum}/AuthenticationStatus.cs (56%) rename src/{Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode => Multifactor.Radius.Adapter.v2.Application/Models/Enum}/PreAuthMode.cs (83%) rename src/{Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode => Multifactor.Radius.Adapter.v2.Application/Models/Enum}/PrivacyMode.cs (87%) rename src/{Multifactor.Radius.Adapter.v2/Core/MultifactorApi => Multifactor.Radius.Adapter.v2.Application/Models/Enum}/RequestStatus.cs (55%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Multifactor.Radius.Adapter.v2.Application.csproj create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs rename src/{Multifactor.Radius.Adapter.v2/Services/Radius => Multifactor.Radius.Adapter.v2.Application/Security}/RadiusPasswordProtector.cs (95%) rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Infrastructure/Adapters}/Ldap/CustomLdapConnectionFactory.cs (73%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs rename src/{Multifactor.Radius.Adapter.v2/Core => Multifactor.Radius.Adapter.v2.Infrastructure/Adapters}/Ldap/LdapConnection.cs (80%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/MultifactorApiResponse.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs rename src/{Multifactor.Radius.Adapter.v2/Server/UdpPacketHandler.cs => Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs} (71%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs rename src/{Multifactor.Radius.Adapter.v2/Services => Multifactor.Radius.Adapter.v2.Infrastructure/Cache}/AuthenticatedClientCache/AuthenticatedClient.cs (90%) rename src/{Multifactor.Radius.Adapter.v2/Services => Multifactor.Radius.Adapter.v2.Infrastructure/Cache}/AuthenticatedClientCache/AuthenticatedClientCache.cs (74%) rename src/{Multifactor.Radius.Adapter.v2/Services => Multifactor.Radius.Adapter.v2.Infrastructure}/Cache/CacheService.cs (75%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius => Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary}/Attributes/DictionaryAttribute.cs (95%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius => Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary}/Attributes/DictionaryVendorAttribute.cs (95%) rename src/{MultiFactor.Radius.Adapter/Core/Radius => Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary}/Attributes/VendorSpecificAttribute.cs (96%) rename src/{Multifactor.Radius.Adapter.v2/Core/Radius/Attributes => Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary}/IRadiusDictionary.cs (83%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure/Configuration => Multifactor.Radius.Adapter.v2.Infrastructure/Configurations}/Exceptions/InvalidConfigurationException.cs (61%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs rename src/{MultiFactor.Radius.Adapter/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Http/BasicAuthHeaderValue.cs (95%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Http/WebProxyFactory.cs (82%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Logging/CustomCompactJsonFormatter.cs (100%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Logging/SerilogJsonFormatterTypes.cs (100%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Logging/SerilogLoggerFactory.cs (77%) rename src/{Multifactor.Radius.Adapter.v2/Infrastructure => Multifactor.Radius.Adapter.v2.Infrastructure}/Logging/StartupLogger.cs (94%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Multifactor.Radius.Adapter.v2.Infrastructure.csproj create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusPacketBuilder.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs rename src/{Multifactor.Radius.Adapter.v2/Services/Radius => Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client}/RadiusClient.cs (95%) rename src/{Multifactor.Radius.Adapter.v2/Services/Radius => Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client}/RadiusClientFactory.cs (77%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusPacketParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/IRadiusPacketValidator.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/Multifactor.Radius.Adapter.v2.Shared.csproj create mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapForestServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeStatus.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeType.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticatedClientCacheConfig.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationState.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/IAuthenticationState.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeDescriptor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthModeSettings.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Auth/UserNameTransformRules.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/ClientConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/DefaultClientConfigurationsProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/Build/IClientConfigurationsProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ClientConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IClientConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/ILdapServerConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/IPermissionRules.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/LdapServerInitializeRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/PermissionRules.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Client/RadiusReplyAttributeValue.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/IServiceConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/Build/ServiceConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/IServiceConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Configuration/Service/ServiceConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/IFirstFactorProcessorProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/ForestFilter.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/IForestFilter.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/Forest/LdapForestEntry.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/ILdapConnectionFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapConnectionStringExtensions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapErrorReasonInfo.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapNamesUtils.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ApiCredential.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/Capabilities.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/ChallengeRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/GroupPolicyPreset.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorApiResponse.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/MultifactorResponse.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyModeDescriptor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ExecutionState.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IExecutionState.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseSender.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/IPipelineExecutionSettings.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Pipeline/Settings/PipelineExecutionSettings.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/RadiusDictionary.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusAttributeCode.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/Metadata/RadiusFieldOffsets.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/Packet/IRadiusPacket.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Radius/SharedSecret.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/RandomWaiterFeature/RandomWaiterConfig.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/UserNameTransformation.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Core/Utils.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Exceptions/LdapUserNotFoundException.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Exceptions/MultifactorApiUnreachableException.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Extensions/ToPascalCaseExtension.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationBuilderExtensions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/ConfigurationExtensions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/IPEndPointFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Build/RadiusAdapterConfigurationProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationDescription.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/RadiusAdapterConfigurationFile.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/AppSettingsSection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServerConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/LdapServer/LdapServersSection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttribute.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplyAttributesSection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/RadiusReply/RadiusReplySection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRule.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesCollection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformRulesSection.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/RadiusAdapter/Sections/UserNameTransform/UserNameTransformSettings.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationEnvironmentVariable.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationFile.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/RadiusConfigurationSource.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAppConfigurationSource.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/XmlAppConfiguration/XmlAssert.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/MultifactorHttpClient.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/IPipelineBuilder.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Builder/PipelineBuilder.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/IPipelineStepsConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineConfigurationFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Configuration/PipelineStepsConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/IRadiusPipelineExecutionContext.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Context/RadiusPipelineExecutionContext.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IPipelineProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/IRadiusPipeline.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/PipelineProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/RadiusPipeline.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessGroupsCheckingStep.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/AccessRequestFilteringStep.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/IRadiusPipelineStep.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/LdapSchemaLoadingStep.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/UserGroupLoadingStep.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Server/IRadiusPacketProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Server/RadiusPacketProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Server/Udp/CustomUdpClient.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpPacketHandler.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/AdapterResponseSender.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/AdapterResponseSender/SendAdapterResponseRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/IAuthenticatedClientCache.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/DataProtection/IDataProtectionService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/DataProtection/LinuxProtectionService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/DataProtection/WindowsProtectionService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ChangeUserPasswordRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/CustomLdapSchemaLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/FindUserProfileRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ActiveDirectoryLdapForestLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestLoaderProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapForestService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/ILdapServerConfigurationService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestLoaderProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapForestService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/Forest/LdapServerConfigurationService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapGroupService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapPasswordChanger.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemaLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapSchemeLoaderWrapper.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapGroupService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapPasswordChanger.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileLoader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapSchemaLoaderWrapper.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/LoadUserGroupsRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/MembershipRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/PasswordChangeResponse.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/CreateSecondFactorRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApi.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/IMultifactorApiService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApi.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/MultifactorApiService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/MultifactorApi/SendChallengeRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/GetReplyAttributesRequest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusAttributeTypeConverter.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusPacketService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/IRadiusReplyAttributeService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusAttributeTypeConverter.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketNasIdentifierParser.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPacketService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusReplyAttributeService.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2/Services/RandomWaiter.cs diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs deleted file mode 100644 index ef30a1b9..00000000 --- a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.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 - -//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; - -namespace MultiFactor.Radius.Adapter.Core.Radius.Attributes -{ - public class DictionaryVendorAttribute : DictionaryAttribute - { - public readonly uint VendorId; - public readonly uint VendorCode; - - - /// - /// Create a dictionary vendor specific attribute - /// - /// - /// - /// - /// - public DictionaryVendorAttribute(uint vendorId, string name, uint vendorCode, string type) : base(name, 26, type) - { - VendorId = vendorId; - VendorCode = vendorCode; - } - } -} 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..1a7443dc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +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 66% rename from src/Multifactor.Radius.Adapter.v2/Services/Cache/ICacheService.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs index 90349d5c..ffcb3fe7 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Cache/ICacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs @@ -1,9 +1,8 @@ -namespace Multifactor.Radius.Adapter.v2.Services.Cache; +namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface ICacheService { 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/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs new file mode 100644 index 00000000..ca179cb4 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs @@ -0,0 +1,36 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class ClientConfiguration +{ + public required string Name { get; init; } + + public string MultifactorNasIdentifier { get; set; } = string.Empty; + public string MultifactorSharedSecret { get; set; } = string.Empty; + public IReadOnlyList SignUpGroups { get; set; } = []; + public bool BypassSecondFactorWhenApiUnreachable { get; set; } = true; + public AuthenticationSource FirstFactorAuthenticationSource { get; set; } + public required IPEndPoint AdapterClientEndpoint { get; set; } + + public IPAddress? RadiusClientIp { get; set; } + public string RadiusClientNasIdentifier { get; set; } = string.Empty; + public string RadiusSharedSecret { get; set; } = string.Empty; + public IPEndPoint[] NpsServerEndpoints { get; set; } + public TimeSpan NpsServerTimeout { get; set; } //"00:00:05" + + public PrivacyMode PrivacyMode { get; set; } + public string[] PrivacyFields { get; set; } = []; + public PreAuthMode? PreAuthenticationMethod { get; set; } + public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; + public (int min, int max) InvalidCredentialDelay { get; set; } + public string CallingStationIdAttribute { get; set; } = string.Empty; + public IReadOnlyList IpWhiteList { get; set; } + public string LoggingLevel { get; set; } = string.Empty; + + public IReadOnlyList? LdapServers { get; set; } + + public IReadOnlyDictionary ReplyAttributes { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs new file mode 100644 index 00000000..88950608 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs @@ -0,0 +1,26 @@ +using Multifactor.Core.Ldap.Name; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class LdapServerConfiguration +{ + 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; } + public bool TrustedDomainsEnabled { get; init; } + public bool AlternativeSuffixesEnabled { get; init; } + public IReadOnlyList IncludedDomains { get; init; } + public IReadOnlyList ExcludedDomains { get; init; } + public IReadOnlyList IncludedSuffixes { get; init; } + public IReadOnlyList ExcludedSuffixes { get; init; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs new file mode 100644 index 00000000..6252c6c6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs @@ -0,0 +1,12 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class RadiusReplyAttribute +{ + public string Name { get; init; } = string.Empty; + public object Value { get; init; } = string.Empty; + public IReadOnlyList UserGroupCondition { get; set; } = []; + public IReadOnlyList UserNameCondition { get; set; } = []; + public bool Sufficient { get; init; } + public bool IsMemberOf => Name?.ToLower() == "memberof"; + public bool FromLdap => !string.IsNullOrWhiteSpace(Name); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs new file mode 100644 index 00000000..6ec400d2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs @@ -0,0 +1,24 @@ +using System.Net; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class RootConfiguration +{ + public required IReadOnlyList MultifactorApiUrls { get; set; } + public string? MultifactorApiProxy { get; set; } + public TimeSpan MultifactorApiTimeout { get; set; } + public required IPEndPoint? AdapterServerEndpoint { get; set; } + + public string LoggingFormat { get; set; } = string.Empty; + public bool SyslogUseTls { get; set; } = false; + public string SyslogServer { get; set; } = string.Empty; + public string SyslogFormat { get; set; } = string.Empty; + public string SyslogFacility { get; set; } = string.Empty; + public string SyslogAppName { get; set; } = "multifactor-radius"; + public string SyslogFramer { get; set; } = string.Empty; + public string SyslogOutputTemplate { get; set; } = string.Empty; + + public string ConsoleLogOutputTemplate { get; set; } = string.Empty; + public string FileLogOutputTemplate { get; set; } = string.Empty; + public int LogFileMaxSizeBytes { get; set; } = 1073741824; +} \ 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..3ce4e674 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -0,0 +1,11 @@ +using System.Net; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public class ServiceConfiguration +{ + public required RootConfiguration RootConfiguration { get; set; } + public required IReadOnlyList ClientsConfigurations { get; set; } + public ClientConfiguration GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.First(config => config.MultifactorNasIdentifier == nasIdentifier); + public ClientConfiguration GetClientConfiguration(IPAddress ip) => ClientsConfigurations.First(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); +} \ No newline at end of file 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..51123870 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs @@ -0,0 +1,74 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +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 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/Core/AccessChallenge/ChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs similarity index 82% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs index de237f7d..803d4507 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs @@ -1,4 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; public class ChallengeProcessorProvider : IChallengeProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs similarity index 56% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs index 85662e99..a9e7a5f5 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChangePasswordChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs @@ -1,34 +1,33 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Security; -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; public class ChangePasswordChallengeProcessor : IChallengeProcessor { private readonly ICacheService _cache; - private readonly ILdapProfileService _ldapService; - private readonly IDataProtectionService _dataProtectionService; + private readonly ILdapAdapter _ldapAdapter; private readonly ILogger _logger; public ChangePasswordChallengeProcessor( ICacheService cache, - ILdapProfileService ldapService, - IDataProtectionService dataProtectionService, + ILdapAdapter ldapAdapter, ILogger logger) { _cache = cache; - _ldapService = ldapService; - _dataProtectionService = dataProtectionService; + _ldapAdapter = ldapAdapter; _logger = logger; } public ChallengeType ChallengeType => ChallengeType.PasswordChange; - public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext context) + public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) { ArgumentNullException.ThrowIfNull(context); if (string.IsNullOrWhiteSpace(context.Passphrase.Password)) @@ -37,9 +36,9 @@ public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext c if (string.IsNullOrWhiteSpace(context.MustChangePasswordDomain)) throw new InvalidOperationException("Domain is required."); - var encryptedPassword = _dataProtectionService.Protect(context.ApiCredential.Pwd, context.Passphrase.Password); + var encryptedPassword = ProtectionService.Protect(context.ClientConfiguration.MultifactorSharedSecret, context.Passphrase.Password); - var passwordRequest = new PasswordChangeRequest() + var passwordRequest = new PasswordChangeCache() { Domain = context.MustChangePasswordDomain, CurrentPasswordEncryptedData = encryptedPassword @@ -49,16 +48,16 @@ public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext c _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); + return new ChallengeIdentifier(context.ClientConfiguration.Name, context.ResponseInformation.State); } public bool HasChallengeContext(ChallengeIdentifier identifier) => _cache.TryGetValue(identifier.RequestId, out _); - public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, IRadiusPipelineExecutionContext context) + public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) { ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(context.UserLdapProfile); - ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration); + ArgumentNullException.ThrowIfNull(context.LdapProfile); + ArgumentNullException.ThrowIfNull(context.LdapConfiguration); ArgumentNullException.ThrowIfNull(context.LdapSchema); var passwordChangeRequest = GetPasswordChangeRequest(identifier.RequestId); @@ -68,43 +67,48 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide if (string.IsNullOrWhiteSpace(context.Passphrase.Raw)) { context.ResponseInformation.ReplyMessage = "Password is empty"; - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; + context.FirstFactorStatus = AuthenticationStatus.Reject; return ChallengeStatus.Reject; } if (string.IsNullOrWhiteSpace(passwordChangeRequest.NewPasswordEncryptedData)) return RepeatPasswordChallenge(context, passwordChangeRequest); - var decryptedNewPassword = _dataProtectionService.Unprotect(context.ApiCredential.Pwd, passwordChangeRequest.NewPasswordEncryptedData); + var decryptedNewPassword = ProtectionService.Unprotect(context.ClientConfiguration.MultifactorSharedSecret, passwordChangeRequest.NewPasswordEncryptedData); if (decryptedNewPassword != context.Passphrase.Raw) return PasswordsNotMatchChallenge(context, passwordChangeRequest); - var request = new ChangeUserPasswordRequest( - decryptedNewPassword, - context.UserLdapProfile, - context.LdapServerConfiguration, - context.LdapSchema); + var request = new ChangeUserPasswordRequest + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + LdapSchema = context.LdapSchema, + DistinguishedName = context.LdapProfile.Dn, + NewPassword = decryptedNewPassword, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds + }; - var result = await _ldapService.ChangeUserPasswordAsync(request); + var success = _ldapAdapter.ChangeUserPassword(request); _cache.Remove(passwordChangeRequest.Id); context.ResponseInformation.State = null; - if (result.Success) + if (success) return ChallengeStatus.Accept; - context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Reject; + context.FirstFactorStatus = AuthenticationStatus.Reject; return ChallengeStatus.Reject; } - private PasswordChangeRequest? GetPasswordChangeRequest(string id) + private PasswordChangeCache? GetPasswordChangeRequest(string id) { - _cache.TryGetValue(id, out PasswordChangeRequest? passwordChangeRequest); + _cache.TryGetValue(id, out PasswordChangeCache? passwordChangeRequest); return passwordChangeRequest; } - private ChallengeStatus PasswordsNotMatchChallenge(IRadiusPipelineExecutionContext request, PasswordChangeRequest passwordChangeRequest) + private ChallengeStatus PasswordsNotMatchChallenge(RadiusPipelineContext request, PasswordChangeCache passwordChangeRequest) { passwordChangeRequest.NewPasswordEncryptedData = null; @@ -116,9 +120,9 @@ private ChallengeStatus PasswordsNotMatchChallenge(IRadiusPipelineExecutionConte return ChallengeStatus.InProcess; } - private ChallengeStatus RepeatPasswordChallenge(IRadiusPipelineExecutionContext context, PasswordChangeRequest passwordChangeRequest) + private ChallengeStatus RepeatPasswordChallenge(RadiusPipelineContext context, PasswordChangeCache passwordChangeRequest) { - passwordChangeRequest.NewPasswordEncryptedData = _dataProtectionService.Protect(context.ApiCredential.Pwd, context.Passphrase.Raw!); + passwordChangeRequest.NewPasswordEncryptedData = ProtectionService.Protect(context.ClientConfiguration.MultifactorSharedSecret, context.Passphrase.Raw!); _cache.Set(passwordChangeRequest.Id, passwordChangeRequest, DateTimeOffset.UtcNow.AddMinutes(5)); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs new file mode 100644 index 00000000..77925ef9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs @@ -0,0 +1,13 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.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/Core/AccessChallenge/IChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs similarity index 57% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs index bfb8f97f..3101d92a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/IChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs @@ -1,4 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; public interface IChallengeProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeIdentifier.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/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/AccessChallenge/Models/ChallengeIdentifier.cs index 890a963b..0abb727c 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/ChallengeIdentifier.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/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.AccessChallenge.Models; public class ChallengeIdentifier : ValueObject { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs new file mode 100644 index 00000000..8948519b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; + +public enum ChallengeStatus +{ + Reject = 0, + InProcess, + Accept +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs new file mode 100644 index 00000000..3079c831 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; + +public enum ChallengeType +{ + None = 0, + SecondFactor, + PasswordChange +} \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs similarity index 66% rename from src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs index 91125d60..0f53c39c 100644 --- a/src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs @@ -1,14 +1,9 @@ -using System; +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; -namespace MultiFactor.Radius.Adapter.Services.Ldap; - -public class PasswordChangeRequest +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/AccessChallenge/Models/PersonalData.cs similarity index 77% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PersonalData.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PersonalData.cs index ab697298..66a56e98 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PersonalData.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PersonalData.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; public class PersonalData { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs similarity index 60% rename from src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs index 246a87b3..bb0465ba 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/AccessChallenge/SecondFactorChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs @@ -1,58 +1,60 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +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.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; public class SecondFactorChallengeProcessor : IChallengeProcessor { // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary _challengeContexts = new(); - private readonly IMultifactorApiService _apiService; - private readonly ILdapGroupService _ldapGroupService; + private readonly ConcurrentDictionary _challengeContexts = new(); + private readonly MultifactorApiService _apiService; + private readonly ILdapAdapter _ldapAdapter; private readonly ILogger _logger; public ChallengeType ChallengeType => ChallengeType.SecondFactor; - public SecondFactorChallengeProcessor(IMultifactorApiService apiAdapter, ILdapGroupService groupService, ILogger logger) + public SecondFactorChallengeProcessor(MultifactorApiService apiAdapter, ILdapAdapter ldapAdapter, ILogger logger) { _apiService = apiAdapter; - _ldapGroupService = groupService; + _ldapAdapter = ldapAdapter; _logger = logger; } - public ChallengeIdentifier AddChallengeContext(IRadiusPipelineExecutionContext context) + public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) { ArgumentNullException.ThrowIfNull(context, nameof(context)); ArgumentException.ThrowIfNullOrWhiteSpace(context.ResponseInformation.State); - var id = new ChallengeIdentifier(context.ClientConfigurationName, context.ResponseInformation.State); + var id = new ChallengeIdentifier(context.ClientConfiguration.Name, 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); + _logger.LogError("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) => _challengeContexts.ContainsKey(identifier); - public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, IRadiusPipelineExecutionContext context) + 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.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); var userName = context.RequestPacket.UserName; if (string.IsNullOrWhiteSpace(userName)) @@ -64,15 +66,15 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide 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)); + var response = await _apiService.SendChallengeAsync(challengeContext, shouldCacheResponse, identifier.RequestId, userAnswer!); return ProcessResponse(context, challengeContext, response, identifier); } - private IRadiusPipelineExecutionContext? GetChallengeContext(ChallengeIdentifier identifier) + private RadiusPipelineContext? GetChallengeContext(ChallengeIdentifier identifier) { - if (_challengeContexts.TryGetValue(identifier, out IRadiusPipelineExecutionContext? request)) + if (_challengeContexts.TryGetValue(identifier, out RadiusPipelineContext? request)) return request; _logger.LogError("Unable to get cached request with state={identifier:l}", identifier); @@ -84,7 +86,7 @@ private void RemoveChallengeContext(ChallengeIdentifier identifier) _challengeContexts.TryRemove(identifier, out _); } - private ChallengeStatus ProcessAuthenticationType(IRadiusPipelineExecutionContext context, UserPassphrase passphrase, string requestId, out string? userAnswer) + private ChallengeStatus ProcessAuthenticationType(RadiusPipelineContext context, UserPassphrase passphrase, string requestId, out string? userAnswer) { userAnswer = string.Empty; switch (context.RequestPacket.AuthenticationType) @@ -98,10 +100,10 @@ private ChallengeStatus ProcessAuthenticationType(IRadiusPipelineExecutionContex _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.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; context.ResponseInformation.State = requestId; return ChallengeStatus.Reject; @@ -117,10 +119,10 @@ private ChallengeStatus ProcessAuthenticationType(IRadiusPipelineExecutionContex "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.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; context.ResponseInformation.State = requestId; return ChallengeStatus.Reject; @@ -135,27 +137,27 @@ private ChallengeStatus ProcessAuthenticationType(IRadiusPipelineExecutionContex "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.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, context.RequestPacket.AuthenticationType); - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; context.ResponseInformation.State = requestId; return ChallengeStatus.Reject; } } - private ChallengeStatus ProcessResponse(IRadiusPipelineExecutionContext context, IRadiusPipelineExecutionContext challengeContext, MultifactorResponse response, ChallengeIdentifier identifier) + 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.UserLdapProfile = challengeContext.UserLdapProfile; - context.AuthenticationState.FirstFactorStatus = challengeContext.AuthenticationState.FirstFactorStatus; - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Accept; + context.LdapProfile = challengeContext.LdapProfile; + context.FirstFactorStatus = challengeContext.FirstFactorStatus; + context.SecondFactorStatus = AuthenticationStatus.Accept; context.Passphrase = challengeContext.Passphrase; RemoveChallengeContext(identifier); @@ -164,8 +166,8 @@ private ChallengeStatus ProcessResponse(IRadiusPipelineExecutionContext context, "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, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, response.Code); return ChallengeStatus.Accept; @@ -176,11 +178,11 @@ private ChallengeStatus ProcessResponse(IRadiusPipelineExecutionContext context, "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, + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port, response.Code); - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; context.ResponseInformation.State = identifier.RequestId; return ChallengeStatus.Reject; @@ -192,34 +194,34 @@ private ChallengeStatus ProcessResponse(IRadiusPipelineExecutionContext context, } } - private ChallengeStatus ProcessEmptyName(IRadiusPipelineExecutionContext context, string requestId) + 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.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + context.SecondFactorStatus = AuthenticationStatus.Reject; context.ResponseInformation.State = requestId; return ChallengeStatus.Reject; } - 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; - var cacheGroups = context.LdapServerConfiguration.AuthenticationCacheGroups; - var isMember = _ldapGroupService.IsMemberOf(new MembershipRequest(context, cacheGroups)); + var cacheGroups = context.LdapConfiguration.AuthenticationCacheGroups; + var isMember = _ldapAdapter.IsMemberOf(MembershipRequest.FromContext(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); + _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; } diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs similarity index 58% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs index f6e4e302..5445f56c 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs @@ -1,7 +1,8 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; public class ActiveDirectoryFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs index 135dbb63..713480fa 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/FreeIpaFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs @@ -1,8 +1,10 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +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; +using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; public class FreeIpaFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs new file mode 100644 index 00000000..60950491 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs @@ -0,0 +1,11 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; + +public interface ILdapBindNameFormatter +{ + LdapImplementation LdapImplementation { get; } + string FormatName(string userName, ILdapProfile ldapProfile); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs similarity index 67% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs index 57edacf8..247a9f16 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/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.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/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs similarity index 85% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs index d59f35f3..758e6575 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/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.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/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs similarity index 60% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs index a19a8f5f..7fa2a307 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs @@ -1,8 +1,10 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +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; +using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; public class MultiDirectoryFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs index f827b27b..06404a35 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/OpenLdapFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs @@ -1,8 +1,10 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +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; +using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; public class OpenLdapFormatter: ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs index 75d8ddd8..e306a6db 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/SambaFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs @@ -1,8 +1,10 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +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; +using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; public class SambaFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/FirstFactorProcessorProvider.cs similarity index 85% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/FirstFactorProcessorProvider.cs index eb87cc2f..f6f1cb3d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/FirstFactorProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/FirstFactorProcessorProvider.cs @@ -1,7 +1,7 @@ using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; public class FirstFactorProcessorProvider : IFirstFactorProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs new file mode 100644 index 00000000..82714a43 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs @@ -0,0 +1,11 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.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/FirstFactor/IFirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs new file mode 100644 index 00000000..03a57b5b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; + +public interface IFirstFactorProcessorProvider +{ + IFirstFactorProcessor GetProcessor(AuthenticationSource authSource); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs new file mode 100644 index 00000000..d5a6ab4c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs @@ -0,0 +1,181 @@ +using System.DirectoryServices.Protocols; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.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 CheckConnectionRequest + { + ConnectionString = serverConfig.ConnectionString, + UserName = bindName, + Password = password, + BindTimeoutInSeconds = serverConfig.BindTimeoutSeconds + }; + _ldapAdapter.CheckConnecion(request); + + 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; + } + 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 void Reject(RadiusPipelineContext context) + { + context.FirstFactorStatus = AuthenticationStatus.Reject; + } + + private void Accept(RadiusPipelineContext context) + { + context.FirstFactorStatus = AuthenticationStatus.Accept; + } + + + private 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/FirstFactor/NoneFirstFactorProcessor.cs similarity index 55% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/NoneFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/NoneFirstFactorProcessor.cs index 925a6836..b648d496 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/NoneFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/NoneFirstFactorProcessor.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.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; public class NoneFirstFactorProcessor : IFirstFactorProcessor { @@ -13,10 +13,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/FirstFactor/RadiusFirstFactorProcessor.cs similarity index 70% rename from src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/RadiusFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/RadiusFirstFactorProcessor.cs index 2f02fa81..0e709945 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/RadiusFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/RadiusFirstFactorProcessor.cs @@ -1,12 +1,14 @@ 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.Features.Pipeline.Models; +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.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; public class RadiusFirstFactorProcessor : IFirstFactorProcessor { @@ -23,7 +25,7 @@ public RadiusFirstFactorProcessor(IRadiusPacketService radiusPacketService, IRad _logger = logger; } - public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) + public async Task ProcessFirstFactor(RadiusPipelineContext context) { Throw.IfNull(context, nameof(context)); @@ -32,24 +34,24 @@ public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) 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 +63,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 +77,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 +85,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 +94,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 +105,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)) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs new file mode 100644 index 00000000..46f98528 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs @@ -0,0 +1,17 @@ +using Multifactor.Core.Ldap.Connection; +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.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap; + +public interface ILdapAdapter +{ + IReadOnlyList LoadUserGroups(LoadUserGroupRequest request); + bool IsMemberOf(MembershipRequest request); + ILdapProfile? FindUserProfile(FindUserRequest request); + bool ChangeUserPassword(ChangeUserPasswordRequest request); + ILdapSchema? LoadSchema(LoadSchemaRequest request); + bool CheckConnecion(CheckConnectionRequest request); +} \ 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..7b7e31a7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs @@ -0,0 +1,15 @@ +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class ChangeUserPasswordRequest +{ + public string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { 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/CheckConnectionRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs new file mode 100644 index 00000000..737c64d3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class CheckConnectionRequest +{ + 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.Application/Features/Ldap/Models/FindUserRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs new file mode 100644 index 00000000..d48a009d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs @@ -0,0 +1,18 @@ +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 string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { 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/Core/Ldap/LdapProfile.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs similarity index 95% 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..65cb6c64 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/LdapProfile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs @@ -4,7 +4,7 @@ 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 { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs new file mode 100644 index 00000000..ab37c523 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs @@ -0,0 +1,22 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class LoadSchemaRequest +{ + public string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { get; set; } + + public static LoadSchemaRequest FromContext(RadiusPipelineContext context) + { + return new LoadSchemaRequest + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, + }; + } +} \ 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..8f170581 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs @@ -0,0 +1,16 @@ +using Multifactor.Core.Ldap.Name; +using Multifactor.Core.Ldap.Schema; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; + +public class LoadUserGroupRequest +{ + public string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { 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..f22f3d0c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs @@ -0,0 +1,36 @@ +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 string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public int BindTimeoutInSeconds { get; set; } + public ILdapSchema LdapSchema { get; set; } + public DistinguishedName DistinguishedName { get; set; } + public DistinguishedName? SearchBase { get; set; } + public DistinguishedName[] TargetGroups { get; set; } + public DistinguishedName[] NestedGroupsBaseDns { get; set; } + + public static MembershipRequest FromContext(RadiusPipelineContext context, IReadOnlyList groups) + { + if (context.LdapConfiguration?.AccessGroups.Count == 0) + throw new ArgumentNullException(); + + return new MembershipRequest + { + ConnectionString = context.LdapConfiguration.ConnectionString, + UserName = context.LdapConfiguration.Username, + Password = context.LdapConfiguration.Password, + LdapSchema = context.LdapSchema, + DistinguishedName = context.LdapProfile.Dn, + BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, + TargetGroups = groups.ToArray(), + NestedGroupsBaseDns = context.LdapConfiguration.NestedGroupsBaseDns.ToArray() + }; + } +} \ No newline at end of file diff --git a/src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs similarity index 89% rename from src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs index d26961f6..acbe4b5f 100644 --- a/src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs @@ -3,9 +3,7 @@ //https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md -using System; - -namespace MultiFactor.Radius.Adapter.Services.MultiFactorApi +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions { [Serializable] internal class MultifactorApiUnreachableException : Exception 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..dbe3ffe4 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,6 @@ 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 bool InlineEnroll { 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 84% 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..9a0cd942 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.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/SecondFactorResponse.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs new file mode 100644 index 00000000..c21c2e74 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs @@ -0,0 +1,15 @@ +using Multifactor.Radius.Adapter.v2.Application.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; } = null; + 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..350492da --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs @@ -0,0 +1,373 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Attributes; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +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.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; + +//TODO separate creation and sending api context +public 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)); + var secondFactorIdentity = GetSecondFactorIdentity(context); + if (string.IsNullOrWhiteSpace(secondFactorIdentity)) + { + _logger.LogWarning("Empty user name for second factor context. Request rejected."); + return new SecondFactorResponse(AuthenticationStatus.Reject); + } + + var personalData = GetPersonalData(context); + + //try to get authenticated client to bypass second factor if configured + 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(personalData, context.ClientConfiguration.PrivacyMode, context.ClientConfiguration.PrivacyFields); + + SecondFactorResponse cloudResponse; + + // TODO move to method + try + { + var phone = context.LdapProfile?.Attributes + .Where(x => context.LdapConfiguration.PhoneAttributes.Contains(x.Name.Value)) + .Select(x => x.GetNotEmptyValues().FirstOrDefault()) + .FirstOrDefault(); + var dto = new AccessRequestQuery + { + Identity = personalData.Identity, + Name = context.LdapProfile.DisplayName, + Email = context.LdapProfile.Email, + Phone = string.IsNullOrWhiteSpace(phone) ? context.LdapProfile?.Phone : phone, + CalledStationId = context.RequestPacket.CalledStationIdAttribute, + CallingStationId = personalData.CallingStationId + }; + var response = await _api.CreateAccessRequest(dto); + 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) + { + cloudResponse = ProcessMfException(apiEx, personalData.Identity, + context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, context.RequestPacket.RemoteEndpoint); + } + catch (Exception ex) + { + cloudResponse = ProcessException(ex, personalData.Identity, context.RequestPacket.RemoteEndpoint); + } + + + if (cloudResponse.Code == AuthenticationStatus.Bypass) + _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", personalData.Identity, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); + + return cloudResponse; + } + + 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 = GetSecondFactorIdentity(context.LdapConfiguration.IdentityAttribute, context.RequestPacket.UserName, context.LdapProfile?.Attributes ?? []); + + 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 = GetCallingStationId(callingStationIdAttr, context.RequestPacket.RemoteEndpoint); + SecondFactorResponse cloudResponse; + + try + { + var response = await _api.SendChallengeAsync(dto); + 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) + { + cloudResponse = ProcessMfException(apiEx, identity, context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, context.RequestPacket.RemoteEndpoint); + } + catch (Exception ex) + { + cloudResponse = ProcessException(ex, identity, context.RequestPacket.RemoteEndpoint); + } + + if (cloudResponse.Code == AuthenticationStatus.Bypass) + _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", identity, + context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.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(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 string? GetSecondFactorIdentity(RadiusPipelineContext context) + { + if (string.IsNullOrWhiteSpace(context.LdapConfiguration.IdentityAttribute)) + return context.RequestPacket.UserName; + + return context.LdapProfile?.Attributes + .FirstOrDefault(x => x.Name == context.LdapConfiguration.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(RadiusPipelineContext context) + { + var secondFactorIdentity = GetSecondFactorIdentity(context); + var callingStationId = context.RequestPacket.CallingStationIdAttribute; + + var callingStationIdForApiRequest = GetCallingStationId(callingStationId, context.RequestPacket.RemoteEndpoint); + + var phone = context.LdapProfile?.Attributes + .Where(x => context.LdapProfile.Phone.Contains(x.Name.Value)) + .Select(x => x.GetNotEmptyValues().FirstOrDefault()) + .FirstOrDefault(); + + var personalData = new PersonalData + { + Identity = secondFactorIdentity!, + DisplayName = context.LdapProfile?.DisplayName, + Email = context.LdapProfile?.Email, + Phone = string.IsNullOrWhiteSpace(phone) ? context.LdapProfile?.Phone : phone, + CalledStationId = context.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 void ApplyPrivacyMode(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, 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 SecondFactorResponse(radCode); + } + + var code = ConvertToAuthCode(AccessRequestResponse.Bypass); + return new SecondFactorResponse(code); + } + + 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..f4b3f7b7 --- /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, CancellationToken cancellationToken = default); + Task SendChallengeAsync(ChallengeRequestQuery query, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs new file mode 100644 index 00000000..09019846 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs @@ -0,0 +1,6 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; + +public interface IPipelineProvider +{ + IRadiusPipeline? GetPipeline(string key); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs new file mode 100644 index 00000000..18fb5cd3 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; + +public interface IRadiusPipeline +{ + Task ExecuteAsync(RadiusPipelineContext context); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs new file mode 100644 index 00000000..83ccce78 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +public interface IRadiusPipelineFactory +{ + IRadiusPipeline CreatePipeline(ClientConfiguration clientConfig); +} \ No newline at end of file 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..7e03c052 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs @@ -0,0 +1,43 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +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.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +public class RadiusPipelineContext +{ + public RadiusPacket RequestPacket { get; } + public ClientConfiguration ClientConfiguration { get; } + public LdapServerConfiguration? 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, + ClientConfiguration clientConfiguration, + LdapServerConfiguration? 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/Core/Ldap/Identity/UserIdentity.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs similarity index 66% rename from src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentity.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs index cbe326cc..d942627c 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Ldap/Identity/UserIdentity.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs @@ -1,6 +1,7 @@ using Multifactor.Core.Ldap.LangFeatures; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; public class UserIdentity { @@ -21,7 +22,7 @@ public UserIdentity(string identity, UserIdentityFormat format) Format = format; } - public UserIdentityFormat GetIdentityTypeByIdentity(string identity) + private UserIdentityFormat GetIdentityTypeByIdentity(string identity) { Throw.IfNullOrWhiteSpace(identity, nameof(identity)); @@ -38,4 +39,13 @@ public UserIdentityFormat GetIdentityTypeByIdentity(string identity) 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/Core/Framework/Context/UserPassphrase.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs similarity index 77% rename from src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs index ee1c8b89..070fde25 100644 --- a/src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs @@ -2,12 +2,10 @@ //Please see licence at //https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md -using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Features.PreAuthModeFeature; -using System; -using System.Linq; using System.Text.RegularExpressions; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -namespace MultiFactor.Radius.Adapter.Core.Framework.Context +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models { public class UserPassphrase { @@ -26,7 +24,7 @@ public class UserPassphrase /// /// 6 digits. /// - public string Otp { get; } + public string? Otp { get; } /// /// Maybe one of 't', 'm', 's' or 'c'.
@@ -51,14 +49,9 @@ private UserPassphrase(string raw, string password, string otp, string providerC ProviderCode = providerCode; } - public static UserPassphrase Parse(string rawPwd, PreAuthModeDescriptor preAuthnMode) + public static UserPassphrase Parse(string rawPwd, PreAuthMode preAuthnMode) { - if (preAuthnMode is null) - { - throw new ArgumentNullException(nameof(preAuthnMode)); - } - - var hasOtp = TryGetOtpCode(rawPwd, preAuthnMode, out var otp); + var hasOtp = TryGetOtpCode(rawPwd, out var otp); if (!hasOtp) { otp = null; @@ -74,13 +67,13 @@ public static UserPassphrase Parse(string rawPwd, PreAuthModeDescriptor preAuthn 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 = 10; if (passwordAndOtp.Length < length) { return passwordAndOtp; @@ -100,10 +93,10 @@ private static string GetPassword(string rawPwd, PreAuthModeDescriptor preAuthnM } } - 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; @@ -111,7 +104,8 @@ private static bool TryGetOtpCode(string rawPwd, PreAuthModeDescriptor preAuthnM } code = passwordAndOtp[^length..]; - if (!Regex.IsMatch(code, preAuthnMode.Settings.OtpCodeRegex)) + var otpCodeRegex = $"^[0-9]{{{length}}}$"; + if (!Regex.IsMatch(code, otpCodeRegex)) { code = null; return false; 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 74% 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..c14cd1bd 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,9 @@ 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.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +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 +15,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 +36,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..5f9da66e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +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 request = MembershipRequest.FromContext(context, context.LdapConfiguration.AccessGroups); + //TODO логику из стараго сервиса сюда и оставить только вызов адаптера + var isMember = _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 60% 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..5952b3c6 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.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.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..e9085d2d 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.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..5edde569 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +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 = LoadSchemaRequest.FromContext(context); + schema = _ldapAdapter.LoadSchema(request); + + if (schema is null) + return schema; + + var expirationDate = DateTimeOffset.Now.AddHours(LdapSchemaCacheLifeTimeInHours); + SaveToCache(cacheKey, schema, expirationDate); + + _logger.LogDebug("LDAP schema for '{domain}' is saved in cache till '{expirationDate}'.", cacheKey, expirationDate.ToString()); + return schema; + } + + 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.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs similarity index 61% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthCheckStep.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs index a8cc9c57..1db316a4 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Pipeline/Steps/PreAuthCheckStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs @@ -1,9 +1,8 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class PreAuthCheckStep : IRadiusPipelineStep { @@ -14,18 +13,18 @@ public PreAuthCheckStep(ILogger logger) _logger = logger; } - public Task ExecuteAsync(IRadiusPipelineExecutionContext context) + public Task ExecuteAsync(RadiusPipelineContext context) { _logger.LogDebug("'{name}' started", nameof(PreAuthCheckStep)); - switch (context.PreAuthnMode.Mode) + switch (context.ClientConfiguration.PreAuthenticationMethod) { case PreAuthMode.Otp when context.Passphrase.Otp == null: - context.AuthenticationState.SecondFactorStatus = AuthenticationStatus.Reject; + 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.RemoteEndpoint.Address, - context.RemoteEndpoint.Port); - context.ExecutionState.Terminate(); + context.RequestPacket.RemoteEndpoint.Address, + context.RequestPacket.RemoteEndpoint.Port); + context.Terminate(); return Task.CompletedTask; case PreAuthMode.None: @@ -35,7 +34,7 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) return Task.CompletedTask; default: - throw new NotImplementedException($"Unknown pre-auth method: {context.PreAuthnMode.Mode}"); + throw new NotImplementedException($"Unknown pre-auth method: {context.ClientConfiguration.PreAuthenticationMethod}"); } } } \ 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..58d49e54 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.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 60% 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..93cc0795 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,40 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models; +using Multifactor.Radius.Adapter.v2.Application.Ports; -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 +57,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)) @@ -72,12 +73,23 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) } _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 + { + 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); //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 +97,19 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) return profile; } - private IEnumerable GetAttributes(IRadiusPipelineExecutionContext context) + private IEnumerable 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 +118,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 55% 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..5f545b3d 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.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +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.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +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 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 = _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 = _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 = _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 59% 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..4840b79f 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,11 @@ 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.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; public class StatusServerFilteringStep : IRadiusPipelineStep { @@ -16,7 +17,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; @@ -28,8 +29,8 @@ 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.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..8fd93807 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs @@ -0,0 +1,150 @@ +using System.DirectoryServices.Protocols; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap; +using Multifactor.Core.Ldap.Connection; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +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 + { + 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 + { + 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 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..1ee404b8 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,9 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.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 +14,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 +25,7 @@ public Task ExecuteAsync(IRadiusPipelineExecutionContext context) return Task.CompletedTask; } - var serverSettings = context.LdapServerConfiguration; + var serverSettings = context.LdapConfiguration; if (serverSettings is null) { @@ -36,7 +35,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 +45,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.IncludedDomains, serverSettings.ExcludedDomains)) { 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 includedDomains, IReadOnlyList excludedDomains) + { + if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain)); + + if (includedDomains.Count > 0) + return includedDomains.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); + + if (excludedDomains.Count > 0) + return excludedDomains.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.Terminate(); context.ResponseInformation.ReplyMessage = replyMessage; } } \ 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..0662da0b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs @@ -0,0 +1,18 @@ +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; + } + + public PipelineNotFoundException(string message, string clientName, Exception innerException) + : base(message, innerException) + { + 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.Application/Features/Radius/Exceptions/RadiusProcessingException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs new file mode 100644 index 00000000..02be99f2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs @@ -0,0 +1,23 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; + +public class RadiusProcessingException : Exception +{ + public string ClientName { get; } + + public RadiusProcessingException(string message) : base(message) { } + + public RadiusProcessingException(string message, Exception innerException) + : base(message, innerException) { } + + public RadiusProcessingException(string message, string clientName) + : base(message) + { + ClientName = clientName; + } + + public RadiusProcessingException(string message, string clientName, Exception innerException) + : base(message, innerException) + { + ClientName = clientName; + } +} \ No newline at end of file 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..da4aaa1b --- /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/Core/Pipeline/IResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs similarity index 62% rename from src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs index 28b511d8..b7596ec9 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; public interface IResponseInformation { 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..be634056 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; -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 70% 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..99f05fca 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,8 @@ 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,6 +10,10 @@ 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; } @@ -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..126e6724 --- /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.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.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 (int min, int max)? 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/Core/Radius/SharedSecret.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs similarity index 96% rename from src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs index cafeca0d..61e8a195 100644 --- a/src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SharedSecret.cs @@ -24,10 +24,9 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -using System; using System.Text; -namespace MultiFactor.Radius.Adapter.Core.Radius +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models { public class SharedSecret { 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.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..b806a7df --- /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, ClientConfiguration 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..6d7098fa --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs @@ -0,0 +1,179 @@ +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +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, ClientConfiguration clientConfiguration) + { + if (requestPacket == null) + throw new ArgumentNullException(nameof(requestPacket)); + if (clientConfiguration == null) + throw new ArgumentNullException(nameof(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(ClientConfiguration clientConfiguration, RadiusPacket requestPacket) + { + bool 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 RadiusProcessingException( + $"All LDAP servers failed for client '{clientConfiguration.Name}'", + lastException); + } + } + + private async Task ExecutePipeline( + ClientConfiguration clientConfiguration, + RadiusPacket requestPacket, + LdapServerConfiguration? 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.Name); + + try + { + await pipeline.ExecuteAsync(context); + + // Отправка ответа + var responseRequest = SendAdapterResponseRequest.FromContext(context); + await _responseSender.SendResponse(responseRequest); + + return true; + } + catch (PipelineNotFoundException ex) + { + // Не логируем как Warning, т.к. это фатальная ошибка конфигурации + _logger.LogError(ex, "Pipeline configuration error for client {ClientName}", clientConfiguration.Name); + throw; + } + catch (Exception ex) + { + // Логируем как Warning, т.к. это временная ошибка выполнения + _logger.LogWarning(ex, + "Pipeline execution failed for client {ClientName}{ServerInfo}", + clientConfiguration.Name, + ldapServerConfiguration != null ? $" with LDAP server {ldapServerConfiguration.ConnectionString}" : ""); + + // Пробрасываем дальше для TryProcessWithLdapServers + throw; + } + } + + private RadiusPipelineContext CreatePipelineContext( + ClientConfiguration clientConfiguration, + RadiusPacket requestPacket, + LdapServerConfiguration? ldapServerConfiguration = null) + { + + var password = requestPacket.TryGetUserPassword(); + var passphrase = UserPassphrase.Parse(password, clientConfiguration.PreAuthenticationMethod.Value); + + var context = new RadiusPipelineContext(requestPacket, clientConfiguration, ldapServerConfiguration) + { + Passphrase = passphrase + }; + + return context; + } + + private IRadiusPipeline GetPipeline(string clientConfigurationName) + { + var pipeline = _pipelineProvider.GetPipeline(clientConfigurationName); + if (pipeline is null) + { + throw new PipelineNotFoundException( + $"No pipeline found for client '{clientConfigurationName}'. " + + "Check adapter configuration and restart the adapter.", + clientConfigurationName); + } + return pipeline; + } + + private bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, ClientConfiguration clientConfiguration) + { + // Если нет LDAP серверов + if (clientConfiguration.LdapServers.Count <= 0) + return true; + + // Если пакет не является AccessRequest + if (requestPacket.Code != PacketCode.AccessRequest) + return true; + + // Дополнительные условия можно добавить здесь + // Например, если есть определенные атрибуты или флаги + + return false; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs similarity index 64% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs index ae3e6cf3..c138a151 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationSource.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth +namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum { [Flags] public enum AuthenticationSource diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationStatus.cs similarity index 56% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationStatus.cs index f0235e4c..94099553 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/AuthenticationStatus.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationStatus.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth; +namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum; public enum AuthenticationStatus { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs similarity index 83% rename from src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs index 0bb539f7..9ec7a750 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Auth/PreAuthMode/PreAuthMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode +namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum { public enum PreAuthMode { diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs similarity index 87% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs index d4e1f5a9..fddf7eed 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/PrivacyMode/PrivacyMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs @@ -2,7 +2,7 @@ //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.Models.Enum; /// /// User information disclosure mode diff --git a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs similarity index 55% rename from src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs index 5a5adace..b98c68a8 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/MultifactorApi/RequestStatus.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Core.MultifactorApi; +namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum; public enum RequestStatus { 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..740eec55 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Multifactor.Radius.Adapter.v2.Application.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs new file mode 100644 index 00000000..dea2a2af --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs @@ -0,0 +1,11 @@ +using System.Net; +using System.Net.Sockets; + +namespace Multifactor.Radius.Adapter.v2.Application.Ports; + +public interface IUdpClient : IDisposable +{ + Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint, + CancellationToken cancellationToken = default); + Task ReceiveAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs new file mode 100644 index 00000000..950aa7ac --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs @@ -0,0 +1,50 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Application.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()) return BytesToString(bytes); + var additionalEntropy = StringToBytes(secret); + return BytesToString(ProtectedData.Unprotect(bytes, additionalEntropy, DataProtectionScope.CurrentUser)); + } + + 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/Radius/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPasswordProtector.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs index 323c921e..3f450b70 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusPasswordProtector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs @@ -1,9 +1,8 @@ using System.Security.Cryptography; using System.Text; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -namespace Multifactor.Radius.Adapter.v2.Services.Radius +namespace Multifactor.Radius.Adapter.v2.Application.Security { public static class RadiusPasswordProtector { diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs index 66536469..fafccd1d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs @@ -6,23 +6,21 @@ 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.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Models; 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.Infrastructure.Radius.Sender; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs index 0aed38d7..ec2d14f8 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs @@ -1,11 +1,13 @@ using System.Net; using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Models; 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; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius; namespace Multifactor.Radius.Adapter.v2.EndToEndTests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs index 250bb4af..cad01743 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs @@ -1,4 +1,4 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs index 180176ed..ae0a829b 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs @@ -1,7 +1,6 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs index e6afd3d9..ba9d8905 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs @@ -1,16 +1,13 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs index 01ea155e..24bafb12 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs @@ -1,7 +1,6 @@ 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.Application.MultifactorApi; using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; @@ -9,7 +8,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs index 25550b56..1a8132dc 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs @@ -1,8 +1,6 @@ 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.Application.MultifactorApi; using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; @@ -10,7 +8,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs index 85f62730..4b9e763d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs @@ -2,15 +2,13 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs index 83db507b..870f357d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs index f2a3eccf..a19cebff 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs index b4062034..02f1669a 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs index 8f05357c..df97630a 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs @@ -1,16 +1,13 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs index f85925f4..7ed86490 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs @@ -1,8 +1,6 @@ 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.Application.MultifactorApi; using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; @@ -10,7 +8,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs index 957ef9d6..a5145192 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs index cce5c320..402595fc 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs index ce3c0901..1125cb45 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs @@ -1,15 +1,12 @@ 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.Application.MultifactorApi; 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; 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 73% 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..8367b2e7 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,9 @@ +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging.Abstractions; 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 { @@ -13,7 +14,7 @@ public CustomLdapConnectionFactory() _factory = LdapConnectionFactory.Create(); } - public CustomLdapConnectionFactory(IEnumerable ldapConnectionFactories) + public CustomLdapConnectionFactory(IEnumerable ldapConnectionFactories) { _factory = new LdapConnectionFactory (NullLogger.Instance, ldapConnectionFactories); } @@ -22,4 +23,6 @@ public ILdapConnection CreateConnection(LdapConnectionOptions ldapConnectionOpti { 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..13766c05 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -0,0 +1,162 @@ +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; +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.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) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + var connection = _connectionFactory.CreateConnection(options); + 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(); + } + + public ILdapProfile? FindUserProfile(FindUserRequest request) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + var connection = _connectionFactory.CreateConnection(options); + var filter = GetFilter(request.UserIdentity, request.LdapSchema); + 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 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") + }; + + public ILdapSchema? LoadSchema(LoadSchemaRequest request) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + return _schemaLoader.Load(options); + } + + public bool CheckConnecion(CheckConnectionRequest request) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + _connectionFactory.CreateConnection(options); + return true; + } + + #region IsMemberOf + public bool IsMemberOf(MembershipRequest request) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + var connection = _connectionFactory.CreateConnection(options); + + 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) + { + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + AuthType.Basic, + request.UserName, + request.Password, + TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + var connection = _connectionFactory.CreateConnection(options); + var changePasswordrequest = BuildPasswordChangeRequest(request.LdapSchema, request.DistinguishedName, request.NewPassword); + var response = connection.SendRequest(changePasswordrequest); + return response.ResultCode == ResultCode.Success; + } + + private 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 + +} \ 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/Models/AccessRequestDto.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs new file mode 100644 index 00000000..8677f390 --- /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; +using Multifactor.Radius.Adapter.v2.Application.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, + Capabilities = new Capabilities{ InlineEnroll = query.InlineEnroll }, + 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..96e0c95e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs @@ -0,0 +1,21 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.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..06ba9e76 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs @@ -0,0 +1,63 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Application.Models; +using Multifactor.Radius.Adapter.v2.Application.Ports; +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 _options = 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, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query, nameof(query)); + + var dto = AccessRequestDto.FromQuery(query); + var client = _clientFactory.CreateClient(_clientName); + var response = await client.PostAsJsonAsync("access/requests/ra", dto, cancellationToken: cancellationToken); + if (!response.IsSuccessStatusCode) + { + var errContent = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogError("Multifactor API request was unsuccessful. Method: access/requests/ra. Reason: {error:l}", errContent); + throw new Exception("Error while requesting access/requests/ra"); + } + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var accessResponse = JsonSerializer.Deserialize>(content, _options); + return accessResponse.Model; + } + + public async Task SendChallengeAsync(ChallengeRequestQuery query, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(query, nameof(query)); + + var dto = ChallengeRequestDto.FromQuery(query); + var client = _clientFactory.CreateClient(_clientName); + var response = await client.PostAsJsonAsync("access/requests/ra/challenge", dto, cancellationToken: cancellationToken); + if (!response.IsSuccessStatusCode) + { + var errContent = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogError("Multifactor API request was unsuccessful. Method: access/requests/ra/challenge: {error:l}", errContent); + throw new Exception("Error while requesting access/requests/ra/challenge"); + } + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var challengeResponse = JsonSerializer.Deserialize>(content, _options); + return challengeResponse.Model; + } + +} \ 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..d4a22f4e 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; + +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; @@ -98,17 +96,17 @@ private bool IsProxyProtocol(byte[] payload, out IPEndPoint sourceEndpoint, out return true; } - private IClientConfiguration? GetClientConfig(UdpReceiveResult udpPacket) + private ClientConfiguration? GetClientConfig(UdpReceiveResult udpPacket) { - IClientConfiguration? clientConfiguration = null; + ClientConfiguration? 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 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..efb58699 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -0,0 +1,133 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Multifactor.Core.Ldap.LangFeatures; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Udp; + +public sealed class CustomUdpClient : IUdpClient +{ + private readonly UdpClient _udpClient; + private readonly ILogger _logger; + private readonly UdpClientOptions _options; + + public CustomUdpClient( + IPEndPoint endPoint, + ILogger logger, + IOptions options) + { + Throw.IfNull(endPoint, nameof(endPoint)); + + _logger = logger; + _options = options?.Value ?? new UdpClientOptions(); + + _udpClient = new UdpClient(); + ConfigureSocket(endPoint); + + _logger?.LogInformation("UDP client initialized on {Endpoint}", endPoint); + } + + private void ConfigureSocket(IPEndPoint endPoint) + { + var socket = _udpClient.Client; + + socket.ReceiveBufferSize = _options.ReceiveBufferSize; + socket.SendBufferSize = _options.SendBufferSize; + + socket.ReceiveTimeout = _options.ReceiveTimeoutMs; + socket.Ttl = _options.Ttl; + socket.DontFragment = true; + + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + socket.Bind(endPoint); + + socket.NoDelay = true; + } + + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + { + try + { + return await _udpClient.ReceiveAsync(cancellationToken); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) + { + throw new OperationCanceledException("UDP receive was interrupted", ex, cancellationToken); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + catch (Exception ex) + { + _logger?.LogError(ex, "UDP receive error"); + throw; + } + } + + public async Task SendAsync( + byte[] datagram, + int bytesCount, + IPEndPoint endPoint, + CancellationToken cancellationToken = default) + { + if (datagram == null) + throw new ArgumentNullException(nameof(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); //TODO разобраться async хочется + 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"); + } + } +} + +public class UdpClientOptions +{ + public int ReceiveBufferSize { get; set; } = 64 * 1024; // 64KB + public int SendBufferSize { get; set; } = 64 * 1024; // 64KB + public int ReceiveTimeoutMs { get; set; } = 0; + public short Ttl { get; set; } = 32; + public bool EnableBroadcast { get; set; } = false; +} \ No newline at end of file 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 90% 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..777a65e2 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 { diff --git a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs similarity index 74% rename from src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs index 57528c96..8a418022 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/AuthenticatedClientCache/AuthenticatedClientCache.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs @@ -1,8 +1,9 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Cache; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -namespace Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Cache.AuthenticatedClientCache; public class AuthenticatedClientCache : IAuthenticatedClientCache { @@ -15,12 +16,11 @@ public AuthenticatedClientCache(ILogger logger) _logger = logger; } - public bool TryHitCache(string? callingStationId, string userName, string clientName, AuthenticatedClientCacheConfig cacheConfig) + public bool TryHitCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime) { - ArgumentNullException.ThrowIfNull(cacheConfig); ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - if (!cacheConfig.Enabled) + if (lifetime == TimeSpan.Zero) return false; if (string.IsNullOrWhiteSpace(callingStationId)) @@ -33,9 +33,9 @@ public bool TryHitCache(string? callingStationId, string userName, string client 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}"); + _logger.LogDebug($"User {userName} with calling-station-id {callingStationId} authenticated {authenticatedClient.Elapsed:hh\\:mm\\:ss} ago. Authentication session period: {lifetime}"); - if (authenticatedClient.Elapsed <= cacheConfig.Lifetime) + if (authenticatedClient.Elapsed <= lifetime) return true; _authenticatedClients.TryRemove(id, out _); @@ -43,12 +43,11 @@ public bool TryHitCache(string? callingStationId, string userName, string client return false; } - public void SetCache(string? callingStationId, string? userName, string clientName, AuthenticatedClientCacheConfig cacheConfig) + public void SetCache(string? callingStationId, string? userName, string clientName, TimeSpan lifetime) { - ArgumentNullException.ThrowIfNull(cacheConfig); ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - if (!cacheConfig.Enabled || string.IsNullOrWhiteSpace(callingStationId)) + if (lifetime == TimeSpan.Zero || string.IsNullOrWhiteSpace(callingStationId)) return; var client = AuthenticatedClient.Create(callingStationId, clientName, userName); @@ -58,7 +57,7 @@ public void SetCache(string? callingStationId, string? userName, string clientNa if (added) { - var expirationDate = DateTimeOffset.Now.Add(cacheConfig.Lifetime); + var expirationDate = DateTimeOffset.Now.Add(lifetime); _logger.LogDebug("Authentication for user '{userName}' is saved in cache till '{expiration}' with key '{key}'", userName, expirationDate.ToString(), client.Id); } else 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/Core/Radius/Attributes/DictionaryAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/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/Dictionary/Attributes/DictionaryAttribute.cs index cfc0a768..5871a693 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/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.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/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/Dictionary/Attributes/DictionaryVendorAttribute.cs index d1cacb7f..7f3d9b1d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/DictionaryVendorAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/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.Dictionary.Attributes { public class DictionaryVendorAttribute : DictionaryAttribute { diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/VendorSpecificAttribute.cs similarity index 96% rename from src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/VendorSpecificAttribute.cs index 8d929a37..e2902960 100644 --- a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/VendorSpecificAttribute.cs @@ -24,9 +24,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -using System; - -namespace MultiFactor.Radius.Adapter.Core.Radius.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.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/Dictionary/IRadiusDictionary.cs similarity index 83% rename from src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/IRadiusDictionary.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/IRadiusDictionary.cs index ca5fb472..43cddef5 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/IRadiusDictionary.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/IRadiusDictionary.cs @@ -1,4 +1,6 @@ -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary { public interface IRadiusDictionary { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs new file mode 100644 index 00000000..5c6c0a9d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs @@ -0,0 +1,140 @@ +using System.Text; +using DictionaryAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryAttribute; +using DictionaryVendorAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryVendorAttribute; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.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 string _filePath; + + public RadiusDictionary(string? filePath = null, string? appPath = null) + { + _filePath = ResolveFilePath(filePath, appPath); + } + + private string ResolveFilePath(string? filePath, string? appPath) + { + if (!string.IsNullOrEmpty(filePath) && Path.IsPathRooted(filePath)) + return filePath; + + var basePath = string.IsNullOrEmpty(appPath) + ? AppDomain.CurrentDomain.BaseDirectory + : 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 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/Configuration/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs similarity index 61% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/Exceptions/InvalidConfigurationException.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs index 28f4dc9e..ed970938 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Configuration/Exceptions/InvalidConfigurationException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -1,8 +1,4 @@ -using System.ComponentModel; -using System.Linq.Expressions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.Exceptions; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; /// /// The Radius adapter configuration is invalid. @@ -51,25 +47,43 @@ protected InvalidConfigurationException( /// 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); - } + /// + /// TODO fix and use or delete + // public static InvalidConfigurationException For(Expression> propertySelector, + // string formattedMessage, + // params object[] args) + // { + // ArgumentNullException.ThrowIfNull(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) + // { + // ArgumentNullException.ThrowIfNull(propertySelector); + // + // if (propertySelector.Body is not MemberExpression { Member: 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; + // return string.IsNullOrWhiteSpace(description) ? property.Name : 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..9ac1d583 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -0,0 +1,84 @@ +using System.Reflection; +using Microsoft.Extensions.Logging; +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.Loader; + +public class ConfigurationLoader : IConfigurationLoader +{ + private readonly IConfigurationParser _parser; + private readonly ILogger _logger; + + public ConfigurationLoader( + IConfigurationParser parser, + ILogger logger) + { + _parser = parser; + _logger = logger; + } + + public async Task LoadAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Loading configuration..."); + + var rootConfig = await LoadRootConfigurationAsync(cancellationToken); + + var clients = await LoadClientConfigurationsAsync(cancellationToken); + + var serviceConfig = new ServiceConfiguration + { + RootConfiguration = rootConfig, + ClientsConfigurations = clients + }; + + _logger.LogInformation("Configuration loaded: {ClientCount} clients", clients.Count); + + return serviceConfig; + } + + private async Task LoadRootConfigurationAsync(CancellationToken ct) + { + var assemblyLocation = Assembly.GetEntryAssembly()?.Location; + var configPath = $"{assemblyLocation}.config"; + + if (!File.Exists(configPath)) + throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); + + return await _parser.ParseRootConfigAsync(configPath, ct); + } + + private async Task> LoadClientConfigurationsAsync( + CancellationToken ct) + { + var clientsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "clients"); + var clients = new List(); + + if (!Directory.Exists(clientsPath)) + { + _logger.LogDebug("No clients directory found at {Path}", clientsPath); + return clients; + } + + var configFiles = Directory.GetFiles(clientsPath, "*.config"); + + foreach (var file in configFiles) + { + try + { + var clientDto = await _parser.ParseClientConfigAsync(file, ct); + clients.Add(clientDto); + + _logger.LogDebug("Loaded client: {Name} from {File}", clientDto.Name, Path.GetFileName(file)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load client configuration from {File}", file); + throw; + } + } + + return clients; + } +} \ 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..f0b899df --- /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 +{ + Task LoadAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs new file mode 100644 index 00000000..1f8c96de --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs @@ -0,0 +1,9 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +public interface IConfigurationParser +{ + Task ParseRootConfigAsync(string filePath, CancellationToken ct); + Task ParseClientConfigAsync(string filePath, CancellationToken ct); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs new file mode 100644 index 00000000..f3f98e2d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs @@ -0,0 +1,25 @@ +using System.Net; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; + +public interface IValueParser +{ + T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct; + bool ParseBool(string? value, bool defaultValue); + int ParseInt(string? value, int defaultValue); + TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null); + TimeSpan ParseTimeout(string? value, TimeSpan defaultValue); + IPEndPoint? ParseEndpoint(string? value, bool required = false); + public IPEndPoint[] ParseEndpoints(string? value, char separator = ';', bool required = false); + Uri? ParseUri(string? value, bool required = false); + IPAddress? ParseIpAddress(string? value, bool required = false); + IReadOnlyList ParseUrls(string? value, bool required = false); + IReadOnlyList ParseIpRanges(string? value); + IReadOnlyList ParseDistinguishedNames(string? value); + IReadOnlyList ParseStringList(string? value, char separator = ';'); + (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value); + (int min, int max) ParseDelaySettings(string value); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs new file mode 100644 index 00000000..ec9b7e45 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs @@ -0,0 +1,262 @@ +using System.Globalization; +using System.Net; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; + +public class ValueParser : IValueParser +{ + private readonly ILogger _logger; + + public ValueParser(ILogger logger) => _logger = logger; + + public T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct + { + if (string.IsNullOrWhiteSpace(value)) + { + return required ? throw new InvalidConfigurationException($"Enum value of type {typeof(T).Name} is required") : defaultValue; + } + + return Enum.TryParse(value, true, out var result) ? result + : throw new InvalidConfigurationException($"Invalid value '{value}' for enum {typeof(T).Name}"); + } + + public bool ParseBool(string? value, bool defaultValue) + { + if (string.IsNullOrWhiteSpace(value)) + return defaultValue; + + return bool.TryParse(value, out var result) ? result : defaultValue; + } + + public int ParseInt(string? value, int defaultValue) + { + if (string.IsNullOrWhiteSpace(value)) + return defaultValue; + + return int.TryParse(value, out var result) ? result : defaultValue; + } + + public TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null) + { + if (string.IsNullOrWhiteSpace(value)) + return defaultValue ?? TimeSpan.Zero; + + if (TimeSpan.TryParse(value, out var result)) + return result; + + throw new InvalidConfigurationException($"Invalid time span format: '{value}'"); + } + + public TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) + { + if (string.IsNullOrWhiteSpace(value)) + return defaultValue; + + var forced = value.EndsWith('!'); + if (forced) + value = value.TrimEnd('!'); + + if (!TimeSpan.TryParseExact(value, @"hh\:mm\:ss", null, TimeSpanStyles.None, out var result)) + return defaultValue; + + if (result == TimeSpan.Zero) + return Timeout.InfiniteTimeSpan; + + // Логирование если timeout слишком маленький + var recommendedMin = TimeSpan.FromSeconds(65); + if (result < recommendedMin) + { + if (forced) + { + _logger.LogWarning( + "Timeout {Timeout}s is less than recommended minimum {Recommended}s", + result.TotalSeconds, recommendedMin.TotalSeconds); + } + else + { + _logger.LogWarning( + "Timeout {Timeout}s is less than recommended minimum {Recommended}s. Use 'value!' to force", + result.TotalSeconds, recommendedMin.TotalSeconds); + result = recommendedMin; + } + } + + return result; + } + + public IPEndPoint? ParseEndpoint(string? value, bool required = false) + { + if (string.IsNullOrWhiteSpace(value)) + { + return required + ? throw new InvalidConfigurationException("Endpoint is required") : null; + } + + return IPEndPoint.TryParse(value, out var endpoint) ? endpoint + : throw new InvalidConfigurationException($"Invalid endpoint format: '{value}'"); + } + + public IPEndPoint[] ParseEndpoints(string? value, char separator, bool required = false) + { + if (string.IsNullOrWhiteSpace(value)) + { + return required + ? throw new InvalidConfigurationException("Endpoints is required") : []; + } + + return value.Split(separator).Select(endpoint => IPEndPoint.TryParse(endpoint, out var endpointResult) ? endpointResult + : throw new InvalidConfigurationException($"Invalid endpoint format: '{endpoint}'")).ToArray(); + + } + + public Uri? ParseUri(string? value, bool required = false) + { + if (string.IsNullOrWhiteSpace(value)) + { + return required ? throw new InvalidConfigurationException("URI is required") : null; + } + + return Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri + : throw new InvalidConfigurationException($"Invalid URI format: '{value}'"); + } + + public IPAddress? ParseIpAddress(string? value, bool required = false) + { + return IPAddress.TryParse(value, out var result) ? result + : throw new InvalidConfigurationException($"Invalid IP address format: '{value}'"); + } + + public IReadOnlyList ParseUrls(string? value, bool required = false) + { + if (string.IsNullOrWhiteSpace(value)) + { + return required ? throw new InvalidConfigurationException("URLs are required") : []; + } + + 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)) + { + urls.Add(uri); + } + else + { + throw new InvalidConfigurationException($"Invalid URL format: '{trimmed}'"); + } + } + + return urls; + } + + public IReadOnlyList ParseIpRanges(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return []; + + 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)) + { + ranges.Add(range); + } + else + { + throw new InvalidConfigurationException($"Invalid IP address range: '{trimmed}'"); + } + } + + return ranges; + } + + public IReadOnlyList ParseDistinguishedNames(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return []; + + 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 ex) + { + throw new InvalidConfigurationException($"Invalid distinguished name: '{part}'", ex); + } + } + + return names; + } + + public IReadOnlyList ParseStringList(string? value, char separator = ';') + { + if (string.IsNullOrWhiteSpace(value)) + return []; + + return value.Split(separator, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + } + + + public (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return (PrivacyMode.None, []); + + var parts = value.Split(':', 2); + var mode = ParseEnum(parts[0], PrivacyMode.None); + + if (parts.Length == 1) + return (mode, []); + + var fields = parts[1].Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(f => f.Trim()) + .Distinct() + .ToArray(); + + return (mode, fields); + } + + public (int min, int max) ParseDelaySettings(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new (0, 0); + } + + if (int.TryParse(value, out var delay)) + { + return delay < 0 ? throw new InvalidConfigurationException($"Invalid delay setting: '{value}'") + : new (delay, delay); + } + + var splitted = value.Split(['-'], StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2) throw new InvalidConfigurationException($"Invalid delay setting: '{value}'"); + + var values = splitted.Select(x => int.TryParse(x, out var d) ? d : -1).ToArray(); + return values.Any(x => x < 0) ? throw new InvalidConfigurationException($"Invalid delay setting: '{value}'") + : new (values[0], values[1]); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs new file mode 100644 index 00000000..9662b8cc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs @@ -0,0 +1,214 @@ +using System.Net; +using System.Xml.Linq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; +using Multifactor.Radius.Adapter.v2.Shared; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +public class XmlConfigurationParser : IConfigurationParser +{ + private readonly IXmlReader _xmlReader; + private readonly IValueParser _valueParser; + private readonly IRadiusDictionary _dictionary; + + public XmlConfigurationParser( + IXmlReader xmlReader, + IValueParser valueParser, + IRadiusDictionary dictionary) + { + _xmlReader = xmlReader; + _valueParser = valueParser; + _dictionary = dictionary; + } + + public async Task ParseRootConfigAsync(string filePath, CancellationToken ct) + { + var xml = await _xmlReader.ReadAsync(filePath, ct); + var settings = _xmlReader.ExtractAppSettings(xml); + + return new RootConfiguration() + { + AdapterServerEndpoint = _valueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-server-endpoint"), required: true), + MultifactorApiUrls = _valueParser.ParseUrls(settings.GetValueOrDefault("multifactor-api-url"), required: true), + MultifactorApiTimeout = _valueParser.ParseTimeout(settings.GetValueOrDefault("multifactor-api-timeout"), + TimeSpan.FromSeconds(65)), + MultifactorApiProxy = settings.GetValueOrDefault("multifactor-api-proxy"), + LoggingFormat = settings.GetValueOrDefault("logging-format") ?? string.Empty, + SyslogUseTls = _valueParser.ParseBool("syslog-use-tls", false), + SyslogServer = settings.GetValueOrDefault("syslog-server") ?? string.Empty, + SyslogFormat = settings.GetValueOrDefault("syslog-format") ?? string.Empty, + SyslogFacility = settings.GetValueOrDefault("syslog-facility") ?? string.Empty, + SyslogAppName = settings.GetValueOrDefault("syslog-app-name") ?? "multifactor-radius", + SyslogFramer = settings.GetValueOrDefault("syslog-framer") ?? string.Empty, + SyslogOutputTemplate = settings.GetValueOrDefault("syslog-output-template") ?? string.Empty, + ConsoleLogOutputTemplate = settings.GetValueOrDefault("console-log-output-template") ?? string.Empty, + FileLogOutputTemplate = settings.GetValueOrDefault("file-log-output-template") ?? string.Empty, + LogFileMaxSizeBytes = _valueParser.ParseInt("log-file-max-size-bytes", 1073741824), + }; + } + + public async Task ParseClientConfigAsync(string filePath, CancellationToken ct) + { + var xml = await _xmlReader.ReadAsync(filePath, ct); + var settings = _xmlReader.ExtractAppSettings(xml); + + var dto = new ClientConfiguration + { + Name = Path.GetFileNameWithoutExtension(filePath), + MultifactorNasIdentifier = settings.GetValueOrDefault("multifactor-nas-identifier") + ?? throw new InvalidConfigurationException("multifactor-nas-identifier is required"), + MultifactorSharedSecret = settings.GetValueOrDefault("multifactor-shared-secret") + ?? throw new InvalidConfigurationException("multifactor-shared-secret is required"), + SignUpGroups = _valueParser.ParseStringList(settings.GetValueOrDefault("sign-up-group")), + BypassSecondFactorWhenApiUnreachable = _valueParser.ParseBool("bypass-second-factor-when-api-unreachable", true), + FirstFactorAuthenticationSource = _valueParser.ParseEnum("first-factor-authentication-source", required: true), + AdapterClientEndpoint = _valueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-client-endpoint"), required: true), + RadiusClientIp = _valueParser.ParseIpAddress(settings.GetValueOrDefault("radius-client-ip")), + RadiusClientNasIdentifier = settings.GetValueOrDefault("radius-client-nas-identifier") ?? string.Empty, + RadiusSharedSecret = settings.GetValueOrDefault("radius-shared-secret") + ?? throw new InvalidConfigurationException("radius-shared-secret is required"), + NpsServerEndpoints = _valueParser.ParseEndpoints(settings.GetValueOrDefault("nps-server-endpoint"), required: true), + NpsServerTimeout = _valueParser.ParseTimeout("nps-server-timeout", TimeSpan.Parse("00:00:05")), + PreAuthenticationMethod = _valueParser.ParseEnum("pre-authentication-method", PreAuthMode.None), + AuthenticationCacheLifetime = _valueParser.ParseTimeSpan( + settings.GetValueOrDefault("authentication-cache-lifetime")), + CallingStationIdAttribute = settings.GetValueOrDefault("calling-station-id-attribute"), + IpWhiteList = _valueParser.ParseIpRanges( + settings.GetValueOrDefault("ip-white-list")), + LoggingLevel = settings.GetValueOrDefault("logging-level"), + InvalidCredentialDelay = _valueParser.ParseDelaySettings(settings.GetValueOrDefault("invalid-credential-delay")), + }; + + var (mode, fields) = _valueParser.ParsePrivacyModeWithFields(settings.GetValueOrDefault("privacy-mode")); + dto.PrivacyMode = mode; + dto.PrivacyFields = fields; + + dto.LdapServers = ParseLdapServers(xml, dto.Name); + + dto.ReplyAttributes = ParseReplyAttributes(xml); + + return dto; + } + + private List ParseLdapServers(XDocument xml, string configName) + { + var servers = new List(); + var ldapElements = xml.Root?.Element("ldapServers")?.Elements("ldapServer"); + + if (ldapElements == null) + return servers; + + servers.AddRange(ldapElements.Select(element => new LdapServerConfiguration + { + ConnectionString = element.Attribute("connection-string")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), + Username = element.Attribute("username")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), + Password = element.Attribute("password")?.Value ?? throw new InvalidConfigurationException("LDAP password is required"), + BindTimeoutSeconds = _valueParser.ParseInt(element.Attribute("bind-timeout-in-seconds")?.Value, 30), + AccessGroups = _valueParser.ParseDistinguishedNames(element.Attribute("access-groups")?.Value), + SecondFaGroups = _valueParser.ParseDistinguishedNames(element.Attribute("second-fa-groups")?.Value), + SecondFaBypassGroups = _valueParser.ParseDistinguishedNames(element.Attribute("second-fa-bypass-groups")?.Value), + LoadNestedGroups = _valueParser.ParseBool(element.Attribute("load-nested-groups")?.Value, true), + NestedGroupsBaseDns = _valueParser.ParseDistinguishedNames(element.Attribute("nested-groups-base-dn")?.Value), + AuthenticationCacheGroups = _valueParser.ParseDistinguishedNames(element.Attribute("authentication-cache-groups")?.Value), + PhoneAttributes = _valueParser.ParseStringList(element.Attribute("phone-attributes")?.Value), + IdentityAttribute = element.Attribute("identity-attribute")?.Value ?? "sAMAccountName", + RequiresUpn = _valueParser.ParseBool(element.Attribute("requires-upn")?.Value, false), + TrustedDomainsEnabled = _valueParser.ParseBool(element.Attribute("enable-trusted-domains")?.Value, false), + AlternativeSuffixesEnabled = _valueParser.ParseBool(element.Attribute("enable-alternative-suffixes")?.Value, false), + IncludedDomains = _valueParser.ParseStringList(element.Attribute("included-domains")?.Value), + ExcludedDomains = _valueParser.ParseStringList(element.Attribute("excluded-domains")?.Value), + IncludedSuffixes = _valueParser.ParseStringList(element.Attribute("included-suffixes")?.Value), + ExcludedSuffixes = _valueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value) + })); + + return servers; + } + + private IReadOnlyDictionary ParseReplyAttributes( + XDocument xml) + { + var elements = _xmlReader.GetRadiusReplyElements(xml); + if (!elements.Any()) + return new Dictionary(); + + var attributeGroups = elements + .Where(e => e.Attribute("name") != null) + .GroupBy(e => e.Attribute("name")!.Value); + + var result = new Dictionary(); + + foreach (var group in attributeGroups) + { + var attributeName = group.Key; + var attributes = new List(); + + foreach (var element in group) + { + var fromAttr = element.Attribute("from")?.Value; + var valueAttr = element.Attribute("value")?.Value; + var whenAttr = element.Attribute("when")?.Value; + var sufficientAttr = element.Attribute("sufficient")?.Value; + + var sufficient = bool.TryParse(sufficientAttr, out var suff) && suff; + + if (!string.IsNullOrEmpty(fromAttr)) + { + attributes.Add(new RadiusReplyAttribute + { + Name = fromAttr, + Sufficient = sufficient + }); + } + else if (!string.IsNullOrEmpty(valueAttr)) + { + var value = ParseRadiusReplyValue(attributeName, valueAttr); + var clauses = whenAttr.Split(['='], StringSplitOptions.RemoveEmptyEntries); + var conditions = clauses[0] switch + { + "UserGroup" or "UserName" => clauses[1] + .Split([';'], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()).ToList(), + _ => throw new Exception($"Unknown condition '{clauses}'") + }; + var attribute = new RadiusReplyAttribute + { + Value = value, + Sufficient = sufficient + }; + if(clauses[0]=="UserGroup") + attribute.UserGroupCondition = conditions; + else attribute.UserNameCondition = conditions; + attributes.Add(attribute); + } + } + + result[attributeName] = attributes.ToArray(); + } + + return result; + } + + private object ParseRadiusReplyValue(string attributeName, string value) + { + var attribute = _dictionary.GetAttribute(attributeName); + if (string.IsNullOrEmpty(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 => value.ToByteArray(), + _ => throw new Exception($"Unknown type {attribute.Type}") + }; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs new file mode 100644 index 00000000..820ff3f9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs @@ -0,0 +1,11 @@ +using System.Xml.Linq; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public interface IXmlReader +{ + Task ReadAsync(string filePath, CancellationToken cancellationToken); + IReadOnlyDictionary ExtractAppSettings(XDocument xml); + IReadOnlyList GetLdapServerElements(XDocument xml); + IReadOnlyList GetRadiusReplyElements(XDocument xml); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs new file mode 100644 index 00000000..85478d17 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs @@ -0,0 +1,64 @@ +using System.Xml; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public class XmlReader : IXmlReader +{ + private readonly ILogger _logger; + public XmlReader(ILogger logger) => _logger = logger; + + public async Task ReadAsync(string filePath, CancellationToken cancellationToken) + { + try + { + _logger.LogDebug("Reading XML configuration from {FilePath}", filePath); + + await using var stream = File.OpenRead(filePath); + var settings = new XmlReaderSettings + { + Async = true, + IgnoreComments = true, + IgnoreWhitespace = true + }; + + using var xmlReader = System.Xml.XmlReader.Create(stream, settings); + var document = await XDocument.LoadAsync(xmlReader, LoadOptions.None, cancellationToken); + + return document; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogError(ex, "Failed to read XML file {FilePath}", filePath); + throw new InvalidConfigurationException($"Failed to read configuration file: {filePath}", ex); + } + } + + public IReadOnlyDictionary ExtractAppSettings(XDocument xml) + { + var appSettings = xml.Root?.Element("appSettings"); + if (appSettings == null) + return new Dictionary(); + + return appSettings.Elements("add") + .Where(e => e.Attribute("key") != null && e.Attribute("value") != null) + .ToDictionary( + e => e.Attribute("key")!.Value, + e => e.Attribute("value")!.Value, + StringComparer.OrdinalIgnoreCase); + } + + public IReadOnlyList GetLdapServerElements(XDocument xml) + { + return xml.Root?.Element("ldapServers")?.Elements("ldapServer").ToList() + ?? []; + } + + public IReadOnlyList GetRadiusReplyElements(XDocument xml) + { + var attributes = xml.Root?.Element("RadiusReply")?.Element("Attributes"); + return attributes?.Elements("add").ToList() ?? []; + } +} \ 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..e478a57a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -0,0 +1,169 @@ +using System.Security.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +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; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor; +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.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; +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.Radius.Client; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; +using Multifactor.Radius.Adapter.v2.Shared; +using Polly; +using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +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(); + services.AddSingleton(); + + services.AddSingleton(provider => + { + var manager = provider.GetRequiredService(); + return manager.LoadAsync(CancellationToken.None).GetAwaiter().GetResult(); + }); + } + + public static IServiceCollection AddRadiusUdpClient(this IServiceCollection services) + { + services.AddSingleton(serviceProvider => + { + var config = serviceProvider.GetRequiredService(); + var endpoint = config.RootConfiguration.AdapterServerEndpoint; + var logger = serviceProvider.GetService>(); + var options = serviceProvider.GetService>(); + + return new CustomUdpClient(endpoint, logger, options); + }); + + return services; + } + + + public static void AddMultifactorApi(this IServiceCollection services) + { + services.AddSingleton(); + services.AddHttpClient("multifactor-api") + .AddPolicyHandler((serviceProvider, request) => + Policy + .Handle() + .OrResult(response => !response.IsSuccessStatusCode) + .FallbackAsync( + fallbackAction: async (outcome, context, cancellationToken) => + { + var urlSelector = serviceProvider.GetRequiredService(); + var fallbackUrl = await urlSelector.GetNextEndpointAsync(); + + var fallbackRequest = request.CloneHttpRequestMessage(); + fallbackRequest.RequestUri = new Uri(new Uri(fallbackUrl), request.RequestUri!.PathAndQuery); + + var httpClientFactory = serviceProvider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient("FallbackClient"); + + return await httpClient.SendAsync(fallbackRequest, cancellationToken); + }, + onFallbackAsync: (outcome, context) => + { + var logger = serviceProvider.GetRequiredService(); + logger.LogWarning("Primary endpoint failed. Trying fallback. Error: {Error}", + outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); + return Task.CompletedTask; + }) + .WrapAsync(Policy.TimeoutAsync(TimeSpan.FromSeconds(10))) + ) + .AddHttpMessageHandler() + .ConfigurePrimaryHttpMessageHandler(provider => + { + var config = provider.GetRequiredService(); + var handler = new HttpClientHandler + { + MaxConnectionsPerServer = 100, + SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 + }; + + if (config.RootConfiguration.MultifactorApiProxy == null) + 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(); + var logger = SerilogLoggerFactory.CreateLogger(serviceConfiguration.RootConfiguration); + Log.Logger = logger; + }); + } + + public static void AddLdap(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + } + + public static void AddInfraServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + } + + public static void AddPipelines(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs new file mode 100644 index 00000000..edd8a002 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs @@ -0,0 +1,45 @@ +namespace Multifactor.Radius.Adapter.v2.Infrastructure.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/Infrastructure/Http/BasicAuthHeaderValue.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs similarity index 95% rename from src/MultiFactor.Radius.Adapter/Infrastructure/Http/BasicAuthHeaderValue.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs index 10f9380a..cb132185 100644 --- a/src/MultiFactor.Radius.Adapter/Infrastructure/Http/BasicAuthHeaderValue.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs @@ -1,7 +1,6 @@ -using System; -using System.Text; +using System.Text; -namespace MultiFactor.Radius.Adapter.Infrastructure.Http +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http { /// /// Represents value (parameter) for a BASIC authentication header. diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs new file mode 100644 index 00000000..5f16d4fa --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs @@ -0,0 +1,23 @@ +namespace Multifactor.Radius.Adapter.v2.Infrastructure.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/Http/RoundRobinEndpointSelector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs new file mode 100644 index 00000000..03064522 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs @@ -0,0 +1,56 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; + +public interface IEndpointSelector +{ + Task GetNextEndpointAsync(); +} + +public class RoundRobinEndpointSelector : IEndpointSelector +{ + private readonly List _endpoints; + private readonly ConcurrentDictionary _failedEndpoints; + private int _currentIndex = -1; + private readonly object _lock = new object(); + private readonly ILogger _logger; + + public RoundRobinEndpointSelector(IConfiguration configuration, + ILogger logger) + { + _endpoints = configuration.GetSection("ApiEndpoints") + .Get>() ?? []; + _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 t 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/Http/WebProxyFactory.cs similarity index 82% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/WebProxyFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/WebProxyFactory.cs index 9d4fac11..0bc446f8 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Http/WebProxyFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/WebProxyFactory.cs @@ -6,13 +6,7 @@ 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/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 77% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogLoggerFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index 98918af8..80e501fb 100644 --- a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -1,6 +1,4 @@ -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 Serilog; using Serilog.Core; using Serilog.Events; @@ -12,7 +10,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Logging; public static class SerilogLoggerFactory { - public static ILogger CreateLogger(RadiusAdapterConfiguration rootConfiguration) + public static ILogger CreateLogger(RootConfiguration rootConfiguration) { ArgumentNullException.ThrowIfNull(rootConfiguration); @@ -25,28 +23,28 @@ public static ILogger CreateLogger(RadiusAdapterConfiguration rootConfiguration) ConfigureLogging( loggerConfiguration, - rootConfiguration.AppSettings.LoggingFormat, - rootConfiguration.AppSettings.SyslogOutputTemplate, - rootConfiguration.AppSettings.ConsoleLogOutputTemplate); + rootConfiguration.LoggingFormat, + rootConfiguration.SyslogOutputTemplate, + 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; - if (string.IsNullOrWhiteSpace(level)) - { - throw InvalidConfigurationException.For(x => x.AppSettings.LoggingLevel, - "'{prop}' element not found. Config name: '{0}'", - RadiusAdapterConfigurationFile.ConfigName); - } - - SetLogLevel(levelSwitch, level); + //TODO rework loglevel + // var level = rootConfiguration.LoggingLevel; + // if (string.IsNullOrWhiteSpace(level)) + // { + // throw InvalidConfigurationException.For(x => x.LoggingLevel, + // "'{prop}' element not found. Config name: '{0}'", + // rootConfiguration.ConfigurationName); + // } + + // SetLogLevel(levelSwitch, level); var logger = loggerConfiguration.CreateLogger(); return logger; @@ -72,10 +70,10 @@ private static void ConfigureLogging( 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)); + // 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)); } return; @@ -204,7 +202,7 @@ private static void SetLogLevel(LoggingLevelSwitch levelSwitch, string level) { SerilogJsonFormatterTypes.Json or SerilogJsonFormatterTypes.JsonUtc => new RenderedCompactJsonFormatter(), SerilogJsonFormatterTypes.JsonTz => new CustomCompactJsonFormatter("yyyy-MM-dd HH:mm:ss.fff zzz"), - SerilogJsonFormatterTypes.ElasticCommonSchema => new EcsTextFormatter(), + // SerilogJsonFormatterTypes.ElasticCommonSchema => new EcsTextFormatter(), _ => null, }; } diff --git a/src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/StartupLogger.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs similarity index 94% rename from src/Multifactor.Radius.Adapter.v2/Infrastructure/Logging/StartupLogger.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs index b5849a6a..1c385f11 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; @@ -46,9 +47,9 @@ public static class StartupLogger /// public static void Information(string message, params object?[] values) => _logger.Value.Information(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..db7e764c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Multifactor.Radius.Adapter.v2.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs new file mode 100644 index 00000000..03dc7f91 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +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; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; + +public class RadiusPipeline : IRadiusPipeline +{ + private readonly List _steps; + private readonly ILogger _logger; + + public RadiusPipeline(List steps) + { + _steps = steps ?? throw new ArgumentNullException(nameof(steps)); + // _logger = logger ?? throw new ArgumentNullException(nameof(logger)); TODO fix or return + } + + public async Task ExecuteAsync(RadiusPipelineContext context) + { + _logger.LogDebug("Starting pipeline execution with {StepCount} steps", _steps.Count); + + foreach (var step in _steps) + { + await step.ExecuteAsync(context); + + if (context.IsTerminated) + { + _logger.LogDebug("Pipeline terminated early at step {StepName}", + step.GetType().Name); + break; + } + } + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs new file mode 100644 index 00000000..35b32211 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +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.Models.Enum; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.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(ClientConfiguration clientConfig) + { + var steps = CreatePipelineSteps(clientConfig); + return new RadiusPipeline(steps); + } + + private List CreatePipelineSteps(ClientConfiguration clientConfig) + { + var steps = new List(); + + steps.Add(CreateStep()); + steps.Add(CreateStep()); + steps.Add(CreateStep()); + + if (clientConfig.LdapServers?.Count > 0) + { + 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 (ShouldLoadUserGroups(clientConfig)) + { + steps.Add(CreateStep()); + } + + return steps; + } + + private IRadiusPipelineStep CreateStep() where TStep : IRadiusPipelineStep + { + return _serviceProvider.GetRequiredService(); + } + + private bool ShouldLoadUserGroups(ClientConfiguration config) => 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.Infrastructure/Pipeline/RadiusPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs new file mode 100644 index 00000000..a8409359 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; + +public class RadiusPipelineProvider : IPipelineProvider +{ + private readonly IRadiusPipelineFactory _pipelineFactory; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _pipelineCache = new(); + private readonly ServiceConfiguration _serviceConfiguration; + + public RadiusPipelineProvider( + IRadiusPipelineFactory pipelineFactory, + ILogger logger, + ServiceConfiguration serviceConfiguration) + { + _pipelineFactory = pipelineFactory; + _logger = logger; + _serviceConfiguration = serviceConfiguration; + } + + public IRadiusPipeline GetPipeline(string clientName) + { + return _pipelineCache.GetOrAdd(clientName, name => + { + _logger.LogDebug("Creating new pipeline for client '{Client}'", name); + return _pipelineFactory.CreatePipeline(GetClientConfiguration(name)); + }); + } + + private ClientConfiguration GetClientConfiguration(string clientName) + { + return _serviceConfiguration.GetClientConfiguration(clientName); + } + + public void ClearCache() + { + _pipelineCache.Clear(); + _logger.LogInformation("Pipeline cache cleared"); + } + + public void RemoveFromCache(string clientName) + { + _pipelineCache.TryRemove(clientName, out _); + _logger.LogDebug("Removed pipeline for client '{Client}' from cache", clientName); + } +} 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/RadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs new file mode 100644 index 00000000..1e57d433 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs @@ -0,0 +1,194 @@ +using System.Net; +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Security; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; + +public interface IRadiusAttributeSerializer +{ + byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null); +} + +public class RadiusAttributeSerializer : IRadiusAttributeSerializer +{ + private readonly IRadiusDictionary _radiusDictionary; + private readonly ILogger _logger; + + public RadiusAttributeSerializer( + IRadiusDictionary radiusDictionary, + ILogger logger) + { + _radiusDictionary = radiusDictionary; + _logger = logger; + } + + public byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null) + { + try + { + var attributeDefinition = _radiusDictionary.GetAttribute(attributeName); + if (attributeDefinition == null) + { + _logger.LogWarning("Unknown attribute: {AttributeName}", attributeName); + return null; + } + + byte[] contentBytes = ConvertValueToBytes(value, attributeDefinition.Type); + + // Special handling for certain attributes + if (attributeDefinition.Code == 2) // User-Password + { + contentBytes = RadiusPasswordProtector.Encrypt(sharedSecret, authenticator, contentBytes); + } + else if (attributeDefinition.Code == 80) // Message-Authenticator + { + // Will be calculated later, fill with zeros for now + contentBytes = new byte[16]; + } + + byte[] headerBytes; + if (attributeDefinition is DictionaryVendorAttribute vendorAttribute) + { + headerBytes = CreateVendorSpecificHeader(vendorAttribute, contentBytes.Length); + } + else + { + headerBytes = CreateStandardHeader(attributeDefinition.Code, contentBytes.Length); + } + + var result = new byte[headerBytes.Length + contentBytes.Length]; + Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); + Buffer.BlockCopy(contentBytes, 0, result, headerBytes.Length, contentBytes.Length); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to serialize attribute: {AttributeName}", attributeName); + return null; + } + } + + private byte[] ConvertValueToBytes(object value, string type) + { + switch (type.ToLowerInvariant()) + { + case "string": + case "tagged-string": + return Encoding.UTF8.GetBytes(value.ToString() ?? string.Empty); + + case "octets": + if (value is byte[] bytes) + return bytes; + if (value is string str) + return Encoding.UTF8.GetBytes(str); + throw new ArgumentException($"Cannot convert {value.GetType()} to octets"); + + case "integer": + case "tagged-integer": + return ConvertIntegerToBytes(value); + + case "ipaddr": + if (value is IPAddress ip) + return ip.GetAddressBytes(); + if (value is string ipStr) + return IPAddress.Parse(ipStr).GetAddressBytes(); + throw new ArgumentException($"Cannot convert {value.GetType()} to IP address"); + + case "date": + return ConvertDateToBytes(value); + + default: + _logger.LogWarning("Unknown attribute type: {Type}", type); + if (value is byte[] byteArray) + return byteArray; + return Encoding.UTF8.GetBytes(value.ToString() ?? string.Empty); + } + } + + private byte[] ConvertIntegerToBytes(object value) + { + int intValue; + + switch (value) + { + case int i: + intValue = i; + break; + case uint ui: + intValue = (int)ui; + break; + case short s: + intValue = s; + break; + case ushort us: + intValue = us; + break; + case byte b: + intValue = b; + break; + case string str when int.TryParse(str, out int parsed): + intValue = parsed; + break; + default: + throw new ArgumentException($"Cannot convert {value.GetType()} to integer"); + } + + var bytes = BitConverter.GetBytes(intValue); + Array.Reverse(bytes); + return bytes; + } + + private byte[] ConvertDateToBytes(object value) + { + DateTime date; + + if (value is DateTime dt) + { + date = dt; + } + else if (value is string str && DateTime.TryParse(str, out var parsed)) + { + date = parsed; + } + else + { + date = DateTime.UtcNow; + } + + var unixTime = (uint)(date - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + var bytes = BitConverter.GetBytes(unixTime); + Array.Reverse(bytes); + return bytes; + } + + private byte[] CreateStandardHeader(byte typeCode, int contentLength) + { + var header = new byte[2]; + header[0] = typeCode; + header[1] = (byte)(2 + contentLength); // Total length: header (2) + content + return header; + } + + private byte[] CreateVendorSpecificHeader(DictionaryVendorAttribute vendorAttribute, int contentLength) + { + // VSA format: Type(1)=26, Length(1), Vendor-Id(4), Vendor-Type(1), Vendor-Length(1), Content + var header = new byte[8]; + + header[0] = 26; // Vendor-Specific attribute type + header[1] = (byte)(8 + contentLength); // Total VSA length + + var vendorIdBytes = BitConverter.GetBytes(vendorAttribute.VendorId); + Array.Reverse(vendorIdBytes); + Buffer.BlockCopy(vendorIdBytes, 0, header, 2, 4); + + header[6] = (byte)vendorAttribute.VendorCode; + header[7] = (byte)(2 + contentLength); // Vendor-specific part length + + return header; + } +} \ 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..e85633c9 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -0,0 +1,172 @@ +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.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +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; + private readonly ILogger _logger; + private readonly IRadiusAttributeSerializer _attributeSerializer; + + public RadiusPacketBuilder( + IRadiusDictionary radiusDictionary, + IRadiusCryptoProvider cryptoProvider, + IRadiusAttributeSerializer attributeSerializer, + ILogger logger) + { + _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); + _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); + _attributeSerializer = attributeSerializer ?? throw new ArgumentNullException(nameof(attributeSerializer)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (sharedSecret == null) throw new ArgumentNullException(nameof(sharedSecret)); + + var packetBytes = new List(); + + // Header: Code (1), Identifier (1), Length (2), Authenticator (16) + packetBytes.Add((byte)packet.Code); + packetBytes.Add(packet.Identifier); + packetBytes.AddRange(new byte[2]); // Placeholder for length + packetBytes.AddRange(new byte[16]); // Placeholder for authenticator + + int messageAuthenticatorPosition = -1; + + // Serialize attributes + foreach (var attribute in packet.Attributes.Values) + { + foreach (var value in attribute.Values) + { + var attributeBytes = _attributeSerializer.Serialize( + attribute.Name, + value, + packet.Authenticator, + sharedSecret, + packet.RequestAuthenticator); + + if (attributeBytes != null) + { + // Check if this is Message-Authenticator + var attributeDefinition = _radiusDictionary.GetAttribute(attribute.Name); + if (attributeDefinition?.Code == 80) // Message-Authenticator + { + messageAuthenticatorPosition = packetBytes.Count; + } + + packetBytes.AddRange(attributeBytes); + } + } + } + + // Set packet length + ushort packetLength = (ushort)packetBytes.Count; + var lengthBytes = BitConverter.GetBytes(packetLength); + Array.Reverse(lengthBytes); // Network byte order + packetBytes[2] = lengthBytes[0]; + packetBytes[3] = lengthBytes[1]; + + 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 != -1) + { + FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret); + } + authenticator = _cryptoProvider.CalculateRequestAuthenticator(sharedSecret, packetBytesArray); + break; + + case PacketCode.StatusServer: + authenticator = packet.RequestAuthenticator != null + ? _cryptoProvider.CalculateResponseAuthenticator( + sharedSecret, + packet.RequestAuthenticator.Value.ToArray(), + packetBytesArray) + : packet.Authenticator.Value.ToArray(); + + if (messageAuthenticatorPosition != -1) + { + FillMessageAuthenticator( + packetBytesArray, + messageAuthenticatorPosition, + sharedSecret, + packet.RequestAuthenticator); + } + break; + + default: + if (packet.RequestAuthenticator != null) + { + authenticator = _cryptoProvider.CalculateResponseAuthenticator( + sharedSecret, + packet.RequestAuthenticator.Value.ToArray(), + packetBytesArray); + } + else + { + authenticator = packet.Authenticator.Value.ToArray(); + } + + if (messageAuthenticatorPosition != -1) + { + FillMessageAuthenticator( + packetBytesArray, + messageAuthenticatorPosition, + sharedSecret, + packet.RequestAuthenticator); + } + break; + } + + // Copy authenticator to packet + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + + 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 FillMessageAuthenticator( + byte[] packetBytes, + int position, + SharedSecret sharedSecret, + RadiusAuthenticator? requestAuthenticator = null) + { + // Zero out the Message-Authenticator field + for (int i = 0; i < 16; i++) + { + packetBytes[position + 2 + i] = 0; + } + + // Calculate and insert Message-Authenticator + 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/Services/Radius/RadiusClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClient.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs index 992c594b..ac021358 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Radius/RadiusClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs @@ -3,8 +3,9 @@ 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.Services.Radius; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; public sealed class RadiusClient : IRadiusClient { @@ -47,8 +48,7 @@ public RadiusClient(IPEndPoint localEndpoint, ILogger logger) { return responseTaskCs.Task.Result.Buffer; } - - //timeout + _logger.LogDebug("Server {remoteEndpoint:l} did not respond within {timeout:l}", remoteEndpoint, timeout.ToString()); return null; } 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..88c70a0d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs @@ -0,0 +1,13 @@ +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); + byte[] EncryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] password); + byte[] 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..ba56e668 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs @@ -0,0 +1,86 @@ +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; + +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) + { + using var hmac = new HMACMD5(secret.Bytes.ToArray()); + + if (requestAuth != null) + { + // For response packets, use request authenticator + var tempPacket = new byte[packet.Length]; + packet.CopyTo(tempPacket, 0); + requestAuth.Value.CopyTo(tempPacket, 4); + return hmac.ComputeHash(tempPacket); + } + + return hmac.ComputeHash(packet); + } + + public bool ValidateMessageAuthenticator( + byte[] packet, + byte[] messageAuth, + int position, + SharedSecret secret, + RadiusAuthenticator? requestAuth = null) + { + var tempPacket = new byte[packet.Length]; + packet.CopyTo(tempPacket, 0); + + for (int i = 0; i < 16; i++) + { + tempPacket[position + 2 + i] = 0; + } + var calculated = CalculateMessageAuthenticator(secret, tempPacket, requestAuth); + + return CryptographicOperations.FixedTimeEquals(calculated, messageAuth); + } + + public byte[] EncryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] password) + { + return RadiusPasswordProtector.Encrypt(secret, authenticator, password); + } + + public byte[] DecryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword) + { + return RadiusPasswordProtector.Decrypt(secret, authenticator, encryptedPassword); + } + + private byte[] CalculateAuthenticator(SharedSecret secret, byte[] packet, byte[] requestAuth) + { + var buffer = new byte[packet.Length + secret.Bytes.Length]; + packet.CopyTo(buffer, 0); + secret.Bytes.CopyTo(buffer.AsMemory(packet.Length)); + + if (requestAuth.Length == 16) + { + requestAuth.CopyTo(buffer, 4); + } + + using var md5 = MD5.Create(); + return md5.ComputeHash(buffer); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs new file mode 100644 index 00000000..1fa94f30 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs @@ -0,0 +1,58 @@ +using System.Security.Cryptography; +using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; + +public static class RadiusPasswordProtector +{ + public static byte[] Encrypt(SharedSecret secret, RadiusAuthenticator authenticator, byte[] password) + { + if (password.Length == 0) + return password; + + var result = new byte[password.Length]; + var paddedLength = ((password.Length + 15) / 16) * 16; + var paddedPassword = new byte[paddedLength]; + password.CopyTo(paddedPassword, 0); + + byte[] lastRound = authenticator.Value.ToArray(); + + for (int i = 0; i < paddedLength; i += 16) + { + using var md5 = MD5.Create(); + md5.TransformBlock(secret.Bytes.ToArray(), 0, secret.Bytes.Length, null, 0); + md5.TransformFinalBlock(lastRound, 0, 16); + lastRound = md5.Hash!; + + for (int j = 0; j < 16; j++) + { + if (i + j < password.Length) + { + result[i + j] = (byte)(paddedPassword[i + j] ^ lastRound[j]); + } + } + } + + return result; + } + + public static byte[] Decrypt(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword) + { + return Encrypt(secret, authenticator, encryptedPassword); // XOR is symmetric + } + + public static string EncryptPasswordString(SharedSecret secret, RadiusAuthenticator authenticator, string password) + { + var passwordBytes = Encoding.UTF8.GetBytes(password); + var encryptedBytes = Encrypt(secret, authenticator, passwordBytes); + return Convert.ToBase64String(encryptedBytes); + } + + public static string DecryptPasswordString(SharedSecret secret, RadiusAuthenticator authenticator, string encryptedPassword) + { + var encryptedBytes = Convert.FromBase64String(encryptedPassword); + var decryptedBytes = Decrypt(secret, authenticator, encryptedBytes); + return Encoding.UTF8.GetString(decryptedBytes).TrimEnd('\0'); + } +} \ 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..543a703a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; + +public interface IRadiusAttributeParser +{ + ParsedAttribute? Parse(byte[] attributeData, 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..c4186893 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -0,0 +1,220 @@ +using System.Net; +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +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 IRadiusCryptoProvider _cryptoProvider; + private readonly ILogger _logger; + + public RadiusAttributeParser( + IRadiusDictionary radiusDictionary, + IRadiusCryptoProvider cryptoProvider, + ILogger logger) + { + _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); + _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public ParsedAttribute? Parse(byte[] attributeData, RadiusAuthenticator authenticator, SharedSecret sharedSecret) + { + if (attributeData == null || attributeData.Length < 2) + return null; + + byte typeCode = attributeData[0]; + byte length = attributeData[1]; + + if (length > attributeData.Length) + return null; + + byte[] contentBytes = new byte[length - 2]; + if (contentBytes.Length > 0) + { + Buffer.BlockCopy(attributeData, 2, contentBytes, 0, contentBytes.Length); + } + + try + { + if (typeCode == 26) // Vendor-Specific + { + return ParseVendorSpecificAttribute(contentBytes, authenticator, sharedSecret); + } + else + { + return ParseStandardAttribute(typeCode, contentBytes, 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 || 2 + vendorLength - 2 > 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 == 80; // Message-Authenticator + + return new ParsedAttribute(attributeDefinition.Name, content, isMessageAuthenticator); + } + + private object? ParseContentBytes( + byte[] contentBytes, + string type, + uint code, + RadiusAuthenticator authenticator, + SharedSecret sharedSecret) + { + switch (type.ToLowerInvariant()) + { + case "string": + case "tagged-string": + return ParseString(contentBytes); + + case "octets": + if (code == 2) // User-Password + { + return _cryptoProvider.DecryptPassword(sharedSecret, authenticator, contentBytes); + } + return contentBytes; + + case "integer": + case "tagged-integer": + return ParseInteger(contentBytes); + + case "ipaddr": + return ParseIpAddress(contentBytes); + + case "date": + return ParseDate(contentBytes); + + case "ifid": + return ParseInterfaceId(contentBytes); + + default: + _logger.LogWarning("Unknown attribute type: {Type}", type); + return contentBytes; + } + } + + private string ParseString(byte[] bytes) + { + // Try to decode as UTF-8, fall back to ASCII if invalid + try + { + return Encoding.UTF8.GetString(bytes).TrimEnd('\0'); + } + catch + { + return Encoding.ASCII.GetString(bytes).TrimEnd('\0'); + } + } + + private int ParseInteger(byte[] bytes) + { + if (bytes.Length == 4) + { + Array.Reverse(bytes); + return BitConverter.ToInt32(bytes, 0); + } + else if (bytes.Length == 2) + { + Array.Reverse(bytes); + return BitConverter.ToInt16(bytes, 0); + } + else if (bytes.Length == 1) + { + return bytes[0]; + } + + throw new InvalidOperationException($"Invalid integer length: {bytes.Length}"); + } + + private IPAddress ParseIpAddress(byte[] bytes) + { + return new IPAddress(bytes); + } + + private DateTime ParseDate(byte[] bytes) + { + Array.Reverse(bytes); + uint seconds = BitConverter.ToUInt32(bytes, 0); + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds); + } + + private byte[] ParseInterfaceId(byte[] bytes) + { + return bytes; // Return as-is for Interface-Id + } +} \ 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..04a1be85 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs @@ -0,0 +1,157 @@ +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.Application.Features.Radius.Ports; +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 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 code = (PacketCode)packetBytes[0]; + var identifier = packetBytes[1]; + var authenticatorBytes = new byte[16]; + Buffer.BlockCopy(packetBytes, 4, authenticatorBytes, 0, 16); + var authenticator = new RadiusAuthenticator(authenticatorBytes); + + var header = new RadiusPacketHeader(code, identifier, authenticator); + var packet = new RadiusPacket(header, requestAuthenticator); + + ParseAttributes(packetBytes, packet, sharedSecret); + + return packet; + } + + private void ValidatePacketLength(byte[] packetBytes) + { + if (packetBytes.Length < 20) + { + throw new InvalidOperationException($"Packet too short: {packetBytes.Length} bytes. Minimum is 20 bytes."); + } + } + + private void ValidatePacketLengthField(byte[] packetBytes) + { + var declaredLength = BitConverter.ToUInt16(new[] { 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 = 20; + int messageAuthenticatorPosition = -1; + byte[] messageAuthenticator = null; + + while (position < packetBytes.Length) + { + if (position + 1 >= packetBytes.Length) + { + throw new InvalidOperationException("Invalid attribute: incomplete header"); + } + + byte typeCode = packetBytes[position]; + byte length = packetBytes[position + 1]; + + if (length < 2) + { + throw new InvalidOperationException($"Invalid attribute length: {length}"); + } + + if (position + length > packetBytes.Length) + { + throw new InvalidOperationException( + $"Attribute exceeds packet boundary. Position: {position}, Length: {length}, Packet Length: {packetBytes.Length}"); + } + + var attributeData = new byte[length]; + Buffer.BlockCopy(packetBytes, position, attributeData, 0, length); + + try + { + var parsedAttribute = _attributeParser.Parse(attributeData, packet.Authenticator, sharedSecret); + + if (parsedAttribute != null) + { + packet.AddAttributeValue(parsedAttribute.Name, parsedAttribute.Value); + + if (parsedAttribute.IsMessageAuthenticator) + { + messageAuthenticatorPosition = position; + if (parsedAttribute.Value is byte[] authBytes) + { + messageAuthenticator = authBytes; + } + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to parse attribute type {TypeCode} at position {Position}", + typeCode, position); + } + + position += length; + } + + if (messageAuthenticatorPosition != -1 && messageAuthenticator != null) + { + 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..108022ae --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -0,0 +1,307 @@ +using System.Text; +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.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Ports; + +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 (ShouldProxyResponse(request)) + { + await ProxyResponseAsync(request); + return; + } + + // Построение и отправка обычного ответа + var responsePacket = BuildResponsePacket(request); + await SendResponsePacketAsync(responsePacket, request); + + LogResponseSent(responsePacket, request); + } + + private bool ShouldProxyResponse(SendAdapterResponseRequest request) + { + // EAP challenge + if (request.ResponsePacket?.IsEapMessageChallenge == true) + return true; + + // Vendor ACL request + if (request.RequestPacket.IsVendorAclRequest && request.ResponsePacket != null) + return true; + + return false; + } + + private async Task ProxyResponseAsync(SendAdapterResponseRequest request) + { + if (request.ResponsePacket == null) + return; + + var logMessage = request.RequestPacket.IsVendorAclRequest + ? "Proxying #ACSACL#" + : "Proxying EAP-Message Challenge"; + + _logger.LogDebug( + "{Action} to {Host}:{Port} id={Id}", + logMessage, + request.RemoteEndpoint.Address, + request.RemoteEndpoint.Port, + request.RequestPacket.Identifier); + + await SendResponsePacketAsync(request.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); + } + + // Добавляем reply-атрибуты + AddReplyAttributes(responsePacket, request); + } + + private void ProcessAccessRejectResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + // Для Reject копируем атрибуты только если это тоже Reject + if (request.ResponsePacket?.Code == PacketCode.AccessReject) + { + CopyAttributes(request.ResponsePacket, responsePacket); + } + } + + private void ProcessAccessChallengeResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + { + // Добавляем State атрибут если есть + 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 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 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 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 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.HasValue) + { + await WaitSomeTimeAsync( + request.InvalidCredentialDelay.Value.min, + request.InvalidCredentialDelay.Value.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..b74477c2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs @@ -0,0 +1,152 @@ +using System.Globalization; +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.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) + { + // Пробуем парсить как обычный IP-адрес + if (IPAddress.TryParse(stringValue, out var ipAddress)) + return ipAddress; + + // Пробуем парсить как Microsoft RADIUS Framed IP Address (целое число) + if (int.TryParse(stringValue, out var intValue)) + return ConvertMsRadiusFramedIpAddress(intValue); + + // Не удалось конвертировать - возвращаем исходную строку + return stringValue; + } + + private 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 object ConvertToDateTime(string stringValue) + { + // Пробуем парсить как DateTime + if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var dateTime)) + { + return dateTime; + } + + // Пробуем парсить как Unix timestamp + if (long.TryParse(stringValue, out var unixTimestamp)) + { + return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime; + } + + return stringValue; + } + + private object ConvertToInteger(string stringValue) + { + if (int.TryParse(stringValue, out var intValue)) + return intValue; + + return stringValue; + } + + private 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/RadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs new file mode 100644 index 00000000..09e12cd5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs @@ -0,0 +1,122 @@ +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 ?? throw new ArgumentNullException(nameof(parser)); + _builder = builder ?? throw new ArgumentNullException(nameof(builder)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + _nasIdentifierExtractor = nasIdentifierExtractor ?? throw new ArgumentNullException(nameof(nasIdentifierExtractor)); + _logger = logger ?? throw new ArgumentNullException(nameof(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) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + if (sharedSecret == null) throw new ArgumentNullException(nameof(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); + } +} + +public interface INasIdentifierExtractor +{ + bool TryExtract(byte[] packetBytes, out string 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..1a8fa803 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -0,0 +1,195 @@ +// Infrastructure/Radius/Services/RadiusReplyAttributeService.cs + +using System.Net; +using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic.CompilerServices; +using Multifactor.Radius.Adapter.v2.Application.Configuration; +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; + +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)); + + var result = new Dictionary>(); + + foreach (var attribute in request.ReplyAttributes) + { + var values = ProcessAttribute(attribute.Key, attribute.Value, request); + if (values.Any()) + { + 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, + RadiusReplyAttribute[] 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(RadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + { + // 1. Проверка LDAP атрибутов + if (attributeValue.FromLdap) + { + if (attributeValue.IsMemberOf) + return request.UserGroups?.Count > 0; + + return !string.IsNullOrEmpty(attributeValue.Name) && + request.HasAttribute(attributeValue.Name); + } + + // 2. Проверка условий по имени пользователя + if (attributeValue.UserNameCondition.Count > 0) + { + return MatchesUserNameCondition(attributeValue.UserNameCondition, request.UserName); + } + + // 3. Проверка условий по группам + if (attributeValue.UserGroupCondition.Count > 0) + { + return MatchesUserGroupCondition(attributeValue.UserGroupCondition, request.UserGroups); + } + + // 4. Без условий - всегда включаем + return true; + } + + private 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 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 List GetAttributeValues(RadiusReplyAttribute 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(RadiusReplyAttribute[] 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 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..3cec2b56 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs @@ -0,0 +1,136 @@ +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.Application.Features.Radius.Ports; + +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), 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"); + + // Check for required attributes based on packet type + switch (packet.Code) + { + case PacketCode.AccessAccept: + case PacketCode.AccessReject: + case PacketCode.AccessChallenge: + // Response packets should have request authenticator + 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) + { + // Access-Request should have User-Name + if (!packet.HasAttribute("User-Name")) + { + _logger.LogWarning("Access-Request missing User-Name attribute"); + } + + // Should have either User-Password or CHAP-Password/Challenge + 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 void ValidateAccountingRequest(RadiusPacket packet) + { + // Accounting-Request should have Acct-Status-Type + if (!packet.HasAttribute("Acct-Status-Type")) + { + throw new InvalidOperationException("Accounting-Request missing Acct-Status-Type"); + } + } + + private void ValidateCoaRequest(RadiusPacket packet) + { + // CoA/Disconnect should have specific attributes + 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/BytesExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs new file mode 100644 index 00000000..9cd0354b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs @@ -0,0 +1,9 @@ +namespace Multifactor.Radius.Adapter.v2.Shared; + +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/HttpRequestMessageExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs new file mode 100644 index 00000000..4e62f88c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs @@ -0,0 +1,47 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Shared; + +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/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.Shared/StringExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs new file mode 100644 index 00000000..c46e088d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs @@ -0,0 +1,69 @@ +using System.Net; +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Shared; + +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.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs index 24325785..bf0c1b88 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs @@ -1,5 +1,6 @@ using Moq; -using Multifactor.Radius.Adapter.v2.Core.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; namespace Multifactor.Radius.Adapter.v2.Tests.AccessChallengeTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs index c0130920..2c4172a6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs @@ -2,18 +2,14 @@ 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.Application.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs index 4c25ae0f..f7c3a0bf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs @@ -1,17 +1,14 @@ 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.Application.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; +using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs index 2e63c1c4..6f3a3dec 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs @@ -1,7 +1,5 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs index e66fb007..3cbde24f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs @@ -1,5 +1,4 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs index 29b6977f..7b800669 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs @@ -1,6 +1,5 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs index ec35b92f..793d8afa 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs @@ -4,7 +4,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs index 4de82e6a..4890f286 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs @@ -2,15 +2,12 @@ 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.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs index 73ae06e2..0702d37b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs @@ -1,13 +1,11 @@ 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.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; 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.Infrastructure.Radius; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs index 4f0cdeab..d887bc7e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs @@ -1,5 +1,6 @@ using System.Reflection; -using Multifactor.Radius.Adapter.v2.Core; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Models; using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; namespace Multifactor.Radius.Adapter.v2.Tests.Fixture; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs index 2b95f6c4..cad06719 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs @@ -4,8 +4,7 @@ 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.Application.Ports.Ldap; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs index f054146d..1a42974f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs @@ -3,8 +3,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs index 5394b700..3c966cfc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs @@ -2,10 +2,9 @@ using Moq; using Multifactor.Core.Ldap.Name; using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; 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.Infrastructure.Adapters.Ldap.Models; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.LdapProfile; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs index 98c5b4bb..22358b84 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs @@ -10,7 +10,7 @@ public class LdapProfileTest [Fact] public void CreateLdapProfile_EntryIsNull_ThrowsArgumentNullException() { - Assert.Throws(() => new Core.Ldap.LdapProfile(null)); + Assert.Throws(() => new Application.Features.Ldap.Models.LdapProfile(null)); } [Fact] @@ -23,7 +23,7 @@ public void CreateLdapProfile_ShouldCreateLdapProfile() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); Assert.NotNull(profile); Assert.Equal(dn, profile.Dn); } @@ -41,7 +41,7 @@ public void CreateLdapProfile_MemberOfAttribute_ShouldReturnMemberOf() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var memberOf = profile.MemberOf.OrderBy(x =>x.StringRepresentation); Assert.NotNull(memberOf); @@ -62,7 +62,7 @@ public void CreateLdapProfile_UpnAttribute_ShouldReturnUpn() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var upnFromProfile = profile.Upn; Assert.Equal(upn, upnFromProfile); @@ -81,7 +81,7 @@ public void CreateLdapProfile_PhoneAttribute_ShouldReturnPhone() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var phoneFromProfile = profile.Phone; Assert.Equal(phone, phoneFromProfile); @@ -100,7 +100,7 @@ public void CreateLdapProfile_EmailAttribute_ShouldReturnEmail() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var emailFromProfile = profile.Email; Assert.NotNull(emailFromProfile); @@ -120,7 +120,7 @@ public void CreateLdapProfile_MailAttribute_ShouldReturnMail() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var emailFromProfile = profile.Email; Assert.NotNull(emailFromProfile); @@ -143,7 +143,7 @@ public void CreateLdapProfile_GetAttributes_ShouldReturnAttributes() var ldapAttrCollection = new LdapAttributeCollection(attributes); var entry = new LdapEntry(dn, ldapAttrCollection); - var profile = new Core.Ldap.LdapProfile(entry); + var profile = new Application.Features.Ldap.Models.LdapProfile(entry); var attributesFromProfile = profile.Attributes; Assert.Equal(2, attributesFromProfile.Count); diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs index a29d19f3..79a2ceef 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs @@ -1,6 +1,5 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs index d2bc2288..28d80037 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs @@ -1,8 +1,7 @@ using System.Diagnostics; using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.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; using Xunit.Abstractions; namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs index e59f0858..12c2f385 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs @@ -1,11 +1,14 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Primitives; using Moq; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps.Challenge; 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; +using AccessChallengeStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.AccessChallengeStep; +using IpWhiteListStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.IpWhiteListStep; +using SecondFactorStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.SecondFactorStep; namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs index a7346dfe..61c815a3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs @@ -1,8 +1,6 @@ using Moq; -using Multifactor.Radius.Adapter.v2.Core.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs index a1546ce0..0718c0a2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs @@ -2,14 +2,9 @@ using Moq; using Multifactor.Core.Ldap.Name; using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs index 01c3f22e..07f649df 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs @@ -1,11 +1,7 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs index 0f103100..4558e08e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs @@ -1,12 +1,9 @@ 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.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs index cc9e190e..ea88f87a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs @@ -1,10 +1,7 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs index e28c1c86..b8098e02 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs @@ -4,12 +4,10 @@ using Multifactor.Core.Ldap.Attributes; using Multifactor.Core.Ldap.Name; using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs index 2ff01422..f71975b7 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs @@ -4,20 +4,14 @@ 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.Application.Features.AccessChallenge; +using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs index 80025ca5..0288cb9a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs @@ -1,11 +1,9 @@ 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; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Models; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; namespace Multifactor.Radius.Adapter.v2.Tests.PipelineTests.StepsTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs index 3c5cefd3..257ddc61 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs @@ -4,13 +4,8 @@ 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.Application.Features.Pipeline; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs index 0cebc535..d32f36af 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Services.Radius; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs index 0513106e..097ffae9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs @@ -1,7 +1,6 @@ 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.Infrastructure.Radius; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs index 3e419dd8..d8c9b1c4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs @@ -1,7 +1,5 @@ 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.Infrastructure.Radius; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs index 9ff3eeb7..b01c386c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs @@ -1,8 +1,6 @@ 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.Infrastructure.Radius; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius.PacketService; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs index b371c6ff..6c2dabac 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs @@ -1,4 +1,3 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs index 656ad9a3..ca9a47dc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs @@ -1,4 +1,4 @@ -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.Radius; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs index 5fd7644c..132e259b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs @@ -2,17 +2,14 @@ using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs index bcc64d7d..d92334c9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs @@ -1,10 +1,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Multifactor.Radius.Adapter.v2.Core; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Models; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; 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; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; namespace Multifactor.Radius.Adapter.v2.Tests.ServiceCollectionExtensionsTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs index 84059ab2..08a7b6c1 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs @@ -2,16 +2,13 @@ using System.Net; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; +using Multifactor.Radius.Adapter.v2.Application.Ports; 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; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; namespace Multifactor.Radius.Adapter.v2.Tests.Unit; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs index 7e50abb5..8d98f2eb 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs @@ -5,15 +5,11 @@ 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.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs index 735fdacc..4f967558 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs @@ -1,14 +1,12 @@ 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.Application.Features.FirstFactor; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Ports; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs index d8de82f8..f0e92632 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs @@ -1,6 +1,6 @@ using Moq; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs index 47e41809..4f21fa88 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs @@ -1,7 +1,7 @@ using Moq; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs index 6d4a3dbe..6e8ab786 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs @@ -1,6 +1,6 @@ using Moq; using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs index 4b559c43..864df9a0 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs @@ -1,7 +1,7 @@ using Moq; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs index d0a1c3bc..5638deed 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs @@ -1,7 +1,7 @@ using Moq; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs index 8cfa352c..5344268b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs @@ -1,7 +1,7 @@ using Moq; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.LdapBindNameFormatterTests; 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 index 428e7349..b416e4e5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs @@ -5,9 +5,7 @@ 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 Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; namespace Multifactor.Radius.Adapter.v2.Tests.Unit; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs index 467d60d6..a28c4178 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs @@ -3,16 +3,12 @@ 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.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; +using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs index 0bc2c5f6..62927f67 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs @@ -1,7 +1,6 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs index 64537f8d..d76c7bd5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs @@ -1,10 +1,8 @@ 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 Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; using NetTools; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.PipelineSteps; 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 index d071bb81..f95da5aa 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs index 6ead5c83..05a838ec 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Net; using Moq; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -using Multifactor.Radius.Adapter.v2.Services.Radius; namespace Multifactor.Radius.Adapter.v2.Tests.Unit; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs index 4c6255cd..3c871add 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs @@ -1,9 +1,10 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Multifactor.Core.Ldap.Attributes; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs index 2000a6c0..cb31bc12 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs @@ -1,4 +1,6 @@ -using Multifactor.Radius.Adapter.v2.Core.Ldap.Identity; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap.Models; using Multifactor.Radius.Adapter.v2.Tests.Fixture; namespace Multifactor.Radius.Adapter.v2.Tests.UserIdentityTests; 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/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/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/ILdapBindNameFormatter.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs deleted file mode 100644 index 9f76dbe3..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core.Ldap; - -namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; - -public interface ILdapBindNameFormatter -{ - LdapImplementation LdapImplementation { get; } - string FormatName(string userName, ILdapProfile ldapProfile); -} \ 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/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/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/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/Attributes/VendorSpecificAttribute.cs b/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs deleted file mode 100644 index 4c16b499..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/Radius/Attributes/VendorSpecificAttribute.cs +++ /dev/null @@ -1,62 +0,0 @@ -//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. - -namespace Multifactor.Radius.Adapter.v2.Core.Radius.Attributes -{ - public class VendorSpecificAttribute - { - public byte Length; - public uint VendorId; - public byte VendorCode; - public Type VendorType; - public byte[] Value; - - - /// - /// Create a vsa from bytes - /// - /// - public VendorSpecificAttribute(byte[] contentBytes) - { - var vendorId = new byte[4]; - Buffer.BlockCopy(contentBytes, 0, vendorId, 0, 4); - Array.Reverse(vendorId); - VendorId = BitConverter.ToUInt32(vendorId, 0); - - var vendorType = new byte[1]; - Buffer.BlockCopy(contentBytes, 4, vendorType, 0, 1); - VendorCode = vendorType[0]; - - var vendorLength = new byte[1]; - Buffer.BlockCopy(contentBytes, 5, vendorLength, 0, 1); - Length = vendorLength[0]; - - var value = new byte[contentBytes.Length - 6]; - Buffer.BlockCopy(contentBytes, 6, value, 0, contentBytes.Length - 6); - Value = value; - } - } -} 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/UserPassphrase.cs b/src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs deleted file mode 100644 index 4218fd84..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Core/UserPassphrase.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Text.RegularExpressions; -using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -namespace Multifactor.Radius.Adapter.v2.Core; - -public class UserPassphrase - { - private static readonly string[] ProviderCodes = { "t", "m", "s", "c" }; - - /// - /// User-Password attribute raw value. - /// - public string? Raw { get; } - - /// - /// User password. - /// - public string? Password { get; } - - /// - /// 6 digits. - /// - public string? Otp { get; } - - /// - /// Maybe one of 't', 'm', 's' or 'c'.
- /// t: Telegram
- /// m: MobileApp
- /// s: SMS
- /// 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; } - - /// - /// 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) - { - Raw = raw; - Password = password; - Otp = otp; - ProviderCode = providerCode; - } - - public static UserPassphrase Parse(string? rawPwd, PreAuthModeDescriptor preAuthnMode) - { - Throw.IfNull(preAuthnMode, nameof(preAuthnMode)); - - var hasOtp = TryGetOtpCode(rawPwd, preAuthnMode, out var otp); - if (!hasOtp) - otp = null; - - var pwd = GetPassword(rawPwd, preAuthnMode, hasOtp); - if (string.IsNullOrWhiteSpace(pwd)) - pwd = null; - - 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) - { - var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; - switch (preAuthnMode.Mode) - { - case PreAuthMode.Otp: - var length = preAuthnMode.Settings.OtpCodeLength; - if (passwordAndOtp.Length < length) - return passwordAndOtp; - - if (!hasOtp) - return passwordAndOtp; - - var sub = passwordAndOtp[..^length]; - return sub; - - case PreAuthMode.None: - default: - return passwordAndOtp; - } - } - - private static bool TryGetOtpCode(string? rawPwd, PreAuthModeDescriptor preAuthnMode, out string? code) - { - var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; - var length = preAuthnMode.Settings.OtpCodeLength; - if (passwordAndOtp.Length < length) - { - code = null; - return false; - } - - code = passwordAndOtp[^length..]; - if (!Regex.IsMatch(code, preAuthnMode.Settings.OtpCodeRegex)) - { - code = null; - return false; - } - - return true; - } - } \ 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/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/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/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..082580e8 100644 --- a/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj +++ b/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj @@ -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..13170578 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 @@ -17,31 +15,25 @@ 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.AddApplicationVariables(); - builder.Services.AddSingleton(appVars); - builder.Services.AddRadiusDictionary(); builder.Services.AddConfiguration(); - builder.Services.AddLdapSchemaLoader(); - builder.Services.AddDataProtectionService(); + builder.Services.AddLdap(); builder.Services.AddFirstFactor(); + builder.Services.AddChallenge(); + builder.Services.AddPipelineSteps(); builder.Services.AddPipelines(); - builder.Services.AddSingleton(); builder.Services.AddTransient(); - builder.Services.AddServices(); + builder.Services.AddInfraServices(); + builder.Services.AddAppServices(); builder.Services.AddChallenge(); - builder.Services.AddUdpClient(); - builder.Services.AddMultifactorHttpClient(); + builder.Services.AddRadiusUdpClient(); + builder.Services.AddMultifactorApi(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs index bef98be4..e5dfbe2e 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -1,111 +1,191 @@ +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; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; +using Multifactor.Radius.Adapter.v2.Application.Ports; namespace Multifactor.Radius.Adapter.v2.Server; -public class AdapterServer : IDisposable +public class AdapterServer : IAsyncDisposable { private readonly IUdpClient _udpClient; - private readonly IUdpPacketHandler _packetHandler; + private readonly IRadiusUdpAdapter _packetAdapter; private readonly ILogger _logger; - private readonly IServiceConfiguration _serviceConfiguration; - private readonly ApplicationVariables _applicationVariables; - private readonly IRadiusDictionary _radiusDictionary; + private readonly ServiceConfiguration _serviceConfiguration; - private bool _isRunning; + private Task? _receiveLoopTask; + private CancellationTokenSource? _cts; + private readonly SemaphoreSlim _concurrencyLimiter; + private readonly ConcurrentBag _activeProcessingTasks = []; + + //Возможно сразу в конфигурацию вынести + private const int ShoutDownTimeout = 30; + private const int MaxConcurrentRequests = 1000; public AdapterServer( IUdpClient udpClient, - IUdpPacketHandler handler, - IServiceConfiguration serviceConfiguration, - ApplicationVariables applicationVariables, - IRadiusDictionary radiusDictionary, + IRadiusUdpAdapter packetAdapter, + ServiceConfiguration serviceConfiguration, ILogger logger) { - _udpClient = udpClient; - _packetHandler = handler; - _serviceConfiguration = serviceConfiguration; - _applicationVariables = applicationVariables; - _radiusDictionary = radiusDictionary; - _logger = logger; - } - - 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); - } - } + _udpClient = udpClient ?? throw new ArgumentNullException(nameof(udpClient)); + _packetAdapter = packetAdapter ?? throw new ArgumentNullException(nameof(packetAdapter)); + _serviceConfiguration = serviceConfiguration ?? throw new ArgumentNullException(nameof(serviceConfiguration)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _concurrencyLimiter = new SemaphoreSlim(MaxConcurrentRequests); } - 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); - _logger.LogInformation("Stopping server"); - _isRunning = false; - _udpClient?.Dispose(); - _logger.LogInformation("Server is stopped"); + LogStartupMessage(); + + 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 (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) + { + 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..."); + + await _cts?.CancelAsync(); + + 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"); + } } - await Task.CompletedTask; + _logger.LogInformation("RADIUS server stopped"); } - private Task ReceivePackets() + private void LogStartupMessage() { - return _udpClient.ReceiveAsync(); + 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..e0a4e3e6 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; namespace Multifactor.Radius.Adapter.v2.Server; @@ -8,40 +7,56 @@ public class ServerHost : IHostedService { private readonly AdapterServer _server; private readonly ILogger _logger; + private Task? _serverTask; + private CancellationTokenSource? _cts; 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) { + _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) { + _logger.LogInformation("Stopping RADIUS server host..."); try { - _server.Stop(); + await _cts?.CancelAsync(); + + if (_serverTask is { IsCompleted: false }) + { + await Task.WhenAny(_serverTask, + Task.Delay(TimeSpan.FromSeconds(30), 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/IUdpClient.cs b/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs deleted file mode 100644 index 197b10a1..00000000 --- a/src/Multifactor.Radius.Adapter.v2/Server/Udp/IUdpClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace Multifactor.Radius.Adapter.v2.Server.Udp; - -public interface IUdpClient : IDisposable -{ - Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint); - Task ReceiveAsync(); -} \ 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/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/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..56300b13 100644 --- a/src/multifactor-radius-adapter.sln +++ b/src/multifactor-radius-adapter.sln @@ -21,6 +21,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter. 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}" EndProject +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 Debug|Any CPU = Debug|Any CPU @@ -47,6 +53,18 @@ Global {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 From 467842f05737057ad2ff921c5d9b6a5fcfd66c01 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 21 Jan 2026 22:23:39 +0300 Subject: [PATCH 02/11] Second iteration --- .../Cache/IAuthenticatedClientCache.cs | 2 - .../Cache/ICacheService.cs | 1 + .../Models/ClientConfiguration.cs | 46 ++-- .../Models/Enum/AuthenticationSource.cs | 2 +- .../Models/Enum/PreAuthMode.cs | 2 +- .../Models/Enum/PrivacyMode.cs | 2 +- .../Models/LdapServerConfiguration.cs | 20 ++ .../Configuration/Models/RootConfiguration.cs | 46 ++-- .../Models/ServiceConfiguration.cs | 4 +- .../Extensions/ApplicationExtensions.cs | 6 +- .../AccessChallenge/Models/ChallengeStatus.cs | 8 - .../AccessChallenge/Models/ChallengeType.cs | 8 - .../IFirstFactorProcessorProvider.cs | 8 - .../Features/Ldap/ILdapAdapter.cs | 3 - .../Features/Ldap/Models/LdapProfile.cs | 2 +- .../Models/AccessRequestResponse.cs | 2 +- .../Multifactor/Models/Enum/RequestStatus.cs | 11 + .../Multifactor/Models/MultifactorAuthData.cs | 15 ++ .../Models/SecondFactorResponse.cs | 2 +- .../Multifactor/MultifactorApiService.cs | 202 ++++++----------- .../Multifactor/Ports/IMultifactorApi.cs | 4 +- .../Multifactor/RequestDataExtractor.cs | 82 +++++++ .../ChallengeProcessorProvider.cs | 4 +- .../ChangePasswordChallengeProcessor.cs | 8 +- .../AccessChallenge/IChallengeProcessor.cs | 4 +- .../IChallengeProcessorProvider.cs | 4 +- .../Models/ChallengeIdentifier.cs | 2 +- .../AccessChallenge/Models/ChallengeStatus.cs | 8 + .../AccessChallenge/Models/ChallengeType.cs | 8 + .../Models/PasswordChangeCache.cs | 2 +- .../AccessChallenge/Models/PersonalData.cs | 2 +- .../SecondFactorChallengeProcessor.cs | 91 ++++++-- .../ActiveDirectoryFormatter.cs | 3 +- .../BindNameFormat/FreeIpaFormatter.cs | 3 +- .../BindNameFormat/ILdapBindNameFormatter.cs | 3 +- .../ILdapBindNameFormatterProvider.cs | 2 +- .../LdapBindNameFormatterProvider.cs | 2 +- .../BindNameFormat/MultiDirectoryFormatter.cs | 3 +- .../BindNameFormat/OpenLdapFormatter.cs | 3 +- .../BindNameFormat/SambaFormatter.cs | 3 +- .../FirstFactorProcessorProvider.cs | 7 +- .../FirstFactor/IFirstFactorProcessor.cs | 4 +- .../IFirstFactorProcessorProvider.cs | 8 + .../FirstFactor/LdapFirstFactorProcessor.cs | 19 +- .../FirstFactor/NoneFirstFactorProcessor.cs | 5 +- .../FirstFactor/RadiusFirstFactorProcessor.cs | 13 +- .../Features/Pipeline/IPipelineProvider.cs | 4 +- .../Pipeline/IRadiusPipelineFactory.cs | 2 +- .../Models/Enum/AuthenticationStatus.cs | 2 +- .../Pipeline/Models/RadiusPipelineContext.cs | 4 +- .../Features/Pipeline/Models/UserIdentity.cs | 12 +- .../Pipeline/Models/UserPassphrase.cs | 2 +- .../Pipeline/Steps/AccessChallengeStep.cs | 4 +- .../Steps/AccessGroupsCheckingStep.cs | 11 +- .../Pipeline/Steps/FirstFactorStep.cs | 8 +- .../Pipeline/Steps/IpWhiteListStep.cs | 2 +- .../Pipeline/Steps/LdapSchemaLoadingStep.cs | 7 +- .../Pipeline/Steps/PreAuthCheckStep.cs | 5 +- .../Pipeline/Steps/PreAuthPostCheck.cs | 2 +- .../Pipeline/Steps/ProfileLoadingStep.cs | 4 +- .../Pipeline/Steps/SecondFactorStep.cs | 16 +- .../Steps/StatusServerFilteringStep.cs | 3 +- .../Pipeline/Steps/UserGroupLoadingStep.cs | 10 +- .../Pipeline/Steps/UserNameValidationStep.cs | 1 - .../Exceptions/RadiusProcessingException.cs | 23 -- .../Features/Radius/Models/RadiusPacket.cs | 1 + .../Radius/Models/RadiusPacketHeader.cs | 2 +- .../Models/SendAdapterResponseRequest.cs | 3 +- .../{ => Features/Radius}/Ports/IUdpClient.cs | 2 +- .../Radius/Services/RadiusPacketProcessor.cs | 27 +-- .../Security/ProtectionService.cs | 2 +- .../Security/RadiusPasswordProtector.cs | 2 +- .../Models/Enum/RequestStatus.cs | 8 - ...actor.Radius.Adapter.v2.Application.csproj | 1 + .../E2EClientConfigurationsProvider.cs | 3 - .../E2ETestBase.cs | 12 +- .../E2ETestsUtils.cs | 4 - .../TestClientConfigsProvider.cs | 4 - .../ConfigLoading/TestRootConfigProvider.cs | 3 - .../Fixtures/Models/E2ERadiusConfiguration.cs | 2 - .../Models/RadiusConfigurationModel.cs | 2 - .../RadiusFixtures.cs | 1 - .../Tests/AccessChallengeTests.cs | 4 - .../Tests/AccessRequestAttributesTests.cs | 5 - .../Tests/BypassWhenApiUnreachableTests.cs | 5 - .../Tests/ChangePasswordTests.cs | 4 - .../Tests/FirstFactorTests.cs | 4 - .../MultipleActiveDirectory2FaGroupsTests.cs | 4 - .../MultipleActiveDirectoryGroupsTests.cs | 4 - .../Tests/PreSecondFactorTests.cs | 4 - .../Tests/ReplyAttributesTests.cs | 5 - ...ingleActiveDirectory2FaBypassGroupTests.cs | 4 - .../SingleActiveDirectory2FaGroupTests.cs | 4 - .../Tests/SingleActiveDirectoryGroupTests.cs | 4 - .../Adapters/Ldap/LdapAdapter.cs | 37 +++- .../Multifactor}/Http/ActivityContext.cs | 2 +- .../Multifactor}/Http/BasicAuthHeaderValue.cs | 2 +- .../Http/MfTraceIdHeaderSetter.cs | 2 +- .../Http/RoundRobinEndpointSelector.cs | 24 +- .../Multifactor}/Http/WebProxyFactory.cs | 2 +- .../Multifactor/Models/AccessRequestDto.cs | 1 - .../Multifactor/Models/ChallengeRequestDto.cs | 1 - .../Adapters/Multifactor/MultifactorApi.cs | 16 +- .../PacketHandler/RadiusUdpAdapter.cs | 6 +- .../Adapters/Udp/CustomUdpClient.cs | 26 +-- .../AuthenticatedClient.cs | 6 +- .../AuthenticatedClientCache.cs | 103 +++++---- .../Dictionary/RadiusDictionary.cs | 15 +- .../InvalidConfigurationException.cs | 76 ------- .../Loader/ConfigurationLoader.cs | 37 ++-- .../Loader/IConfigurationLoader.cs | 1 + .../Parser/{ValueParser => }/ValueParser.cs | 57 ++--- .../Parser/ValueParser/IValueParser.cs | 25 --- .../Parser/XmlConfigurationParser.cs | 99 ++++----- .../Configurations/Reader/IXmlReader.cs | 11 - .../Configurations/Reader/XmlReader.cs | 15 +- .../Extensions/InfrastructureExtensions.cs | 57 +++-- .../Logging/SerilogLoggerFactory.cs | 32 +-- .../Logging/StartupLogger.cs | 27 +-- ...or.Radius.Adapter.v2.Infrastructure.csproj | 3 +- .../Pipeline/RadiusPipeline.cs | 8 +- .../Pipeline/RadiusPipelineFactory.cs | 34 ++- .../Pipeline/RadiusPipelineProvider.cs | 23 +- .../Builders/IRadiusAttributeSerializer.cs | 8 + .../Builders/RadiusAttributeSerializer.cs | 14 +- .../Radius/Builders/RadiusPacketBuilder.cs | 2 - .../Radius/Client/RadiusClient.cs | 206 +++++++++++++++--- .../Radius/Crypto/IRadiusCryptoProvider.cs | 1 - .../Radius/Crypto/RadiusCryptoProvider.cs | 31 +-- .../Radius/Crypto/RadiusPasswordProtector.cs | 15 -- .../Radius/Parsers/RadiusAttributeParser.cs | 43 ++-- .../Radius/Parsers/RadiusPacketParser.cs | 7 +- .../Radius/Sender/AdapterResponseSender.cs | 31 +-- .../Services/RadiusAttributeTypeConverter.cs | 18 +- .../Services/RadiusNasIdentifierExtractor.cs | 63 ++++++ .../Radius/Services/RadiusPacketService.cs | 5 - .../Services/RadiusReplyAttributeService.cs | 14 +- .../Validators/RadiusPacketValidator.cs | 11 +- .../Attributes/ConfigAttribute.cs | 33 +++ .../{ => Extensions}/BytesExtensions.cs | 2 +- .../HttpRequestMessageExtension.cs | 2 +- .../{ => Extensions}/StringExtension.cs | 3 +- 142 files changed, 1145 insertions(+), 1053 deletions(-) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Configuration}/Models/Enum/AuthenticationSource.cs (59%) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Configuration}/Models/Enum/PreAuthMode.cs (80%) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Configuration}/Models/Enum/PrivacyMode.cs (86%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/Enum/RequestStatus.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/MultifactorAuthData.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/ChallengeProcessorProvider.cs (80%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/ChangePasswordChallengeProcessor.cs (94%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/IChallengeProcessor.cs (73%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/IChallengeProcessorProvider.cs (54%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/Models/ChallengeIdentifier.cs (90%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/Models/PasswordChangeCache.cs (73%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/Models/PersonalData.cs (75%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/AccessChallenge/SecondFactorChallengeProcessor.cs (74%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs (72%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/FreeIpaFormatter.cs (84%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs (64%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs (65%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs (84%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs (84%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/OpenLdapFormatter.cs (84%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/BindNameFormat/SambaFormatter.cs (84%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/FirstFactorProcessorProvider.cs (74%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/IFirstFactorProcessor.cs (66%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessorProvider.cs rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/LdapFirstFactorProcessor.cs (91%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/NoneFirstFactorProcessor.cs (77%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/{ => Pipeline}/FirstFactor/RadiusFirstFactorProcessor.cs (91%) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Features/Pipeline}/Models/Enum/AuthenticationStatus.cs (50%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs rename src/Multifactor.Radius.Adapter.v2.Application/{ => Features/Radius}/Ports/IUdpClient.cs (80%) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Features}/Security/ProtectionService.cs (95%) rename src/Multifactor.Radius.Adapter.v2.Application/{ => Features}/Security/RadiusPasswordProtector.cs (97%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs rename src/Multifactor.Radius.Adapter.v2.Infrastructure/{ => Adapters/Multifactor}/Http/ActivityContext.cs (93%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/{ => Adapters/Multifactor}/Http/BasicAuthHeaderValue.cs (96%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/{ => Adapters/Multifactor}/Http/MfTraceIdHeaderSetter.cs (88%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/{ => Adapters/Multifactor}/Http/RoundRobinEndpointSelector.cs (62%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/{ => Adapters/Multifactor}/Http/WebProxyFactory.cs (93%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/{ValueParser => }/ValueParser.cs (78%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusNasIdentifierExtractor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs rename src/Multifactor.Radius.Adapter.v2.Shared/{ => Extensions}/BytesExtensions.cs (74%) rename src/Multifactor.Radius.Adapter.v2.Shared/{ => Extensions}/HttpRequestMessageExtension.cs (96%) rename src/Multifactor.Radius.Adapter.v2.Shared/{ => Extensions}/StringExtension.cs (96%) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs index 1a7443dc..9085699c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/IAuthenticatedClientCache.cs @@ -1,5 +1,3 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; - namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface IAuthenticatedClientCache diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs index ffcb3fe7..bdf5b632 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs @@ -2,6 +2,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface ICacheService { + //TODO check how use. void Set(string key, T value, DateTimeOffset expirationDate); bool TryGetValue(string key, out T? value); void Remove(string key); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs index ca179cb4..3e0624bd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs @@ -1,36 +1,54 @@ using System.Net; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +using Multifactor.Radius.Adapter.v2.Shared.Attributes; using NetTools; namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public class ClientConfiguration { - public required string Name { get; init; } + public string Name { get; set; } - public string MultifactorNasIdentifier { get; set; } = string.Empty; - public string MultifactorSharedSecret { get; set; } = string.Empty; + [ConfigParameter("multifactor-nas-identifier")] + public string MultifactorNasIdentifier { get; set; } + [ConfigParameter("multifactor-shared-secret")] + public string MultifactorSharedSecret { get; set; } + [ConfigParameter("sign-up-group")] public IReadOnlyList SignUpGroups { get; set; } = []; - public bool BypassSecondFactorWhenApiUnreachable { get; set; } = true; + [ConfigParameter("bypass-second-factor-when-api-unreachable",true)] + public bool BypassSecondFactorWhenApiUnreachable { get; set; } + [ConfigParameter("first-factor-authentication-source")] public AuthenticationSource FirstFactorAuthenticationSource { get; set; } - public required IPEndPoint AdapterClientEndpoint { get; set; } + [ConfigParameter("adapter-client-endpoint")] + public IPEndPoint AdapterClientEndpoint { get; set; } + [ConfigParameter("radius-client-ip")] public IPAddress? RadiusClientIp { get; set; } - public string RadiusClientNasIdentifier { get; set; } = string.Empty; - public string RadiusSharedSecret { get; set; } = string.Empty; + [ConfigParameter("radius-client-nas-identifier")] + public string RadiusClientNasIdentifier { get; set; } + [ConfigParameter("radius-shared-secret")] + public string RadiusSharedSecret { get; set; } + [ConfigParameter("nps-server-endpoint")] public IPEndPoint[] NpsServerEndpoints { get; set; } + [ConfigParameter("nps-server-timeout")] public TimeSpan NpsServerTimeout { get; set; } //"00:00:05" - public PrivacyMode PrivacyMode { get; set; } - public string[] PrivacyFields { get; set; } = []; + [ConfigParameter("privacy-mode")] + public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } + + [ConfigParameter("pre-authentication-method")] public PreAuthMode? PreAuthenticationMethod { get; set; } + [ConfigParameter("authentication-cache-lifetime")] public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; + [ConfigParameter("invalid-credential-delay")] public (int min, int max) InvalidCredentialDelay { get; set; } - public string CallingStationIdAttribute { get; set; } = string.Empty; + [ConfigParameter("calling-station-id-attribute")] + public string CallingStationIdAttribute { get; set; } + [ConfigParameter("ip-white-list")] public IReadOnlyList IpWhiteList { get; set; } - public string LoggingLevel { get; set; } = string.Empty; + [ComplexConfigParameter("ldapServers")] public IReadOnlyList? LdapServers { get; set; } - - public IReadOnlyDictionary ReplyAttributes { get; set; } + [ComplexConfigParameter("RadiusReply")] + public IReadOnlyDictionary? ReplyAttributes { get; set; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/AuthenticationSource.cs similarity index 59% rename from src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/AuthenticationSource.cs index c138a151..f7cc374c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationSource.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/AuthenticationSource.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum { [Flags] public enum AuthenticationSource diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs similarity index 80% rename from src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs index 9ec7a750..450cf373 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PreAuthMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum { public enum PreAuthMode { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs similarity index 86% rename from src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs index fddf7eed..f6ee35b3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/PrivacyMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs @@ -2,7 +2,7 @@ //Please see licence at //https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md -namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum; +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; /// /// User information disclosure mode diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs index 88950608..41f9d286 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs @@ -1,26 +1,46 @@ using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Shared.Attributes; namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public class LdapServerConfiguration { + [ConfigParameter("connection-string")] public string ConnectionString { get; init; } + [ConfigParameter("username")] public string Username { get; init; } + [ConfigParameter("password")] public string Password { get; init; } + [ConfigParameter("bind-timeout-in-seconds")] public int BindTimeoutSeconds{ get; init; } + [ConfigParameter("access-groups")] public IReadOnlyList AccessGroups { get; init; } + [ConfigParameter("second-fa-groups")] public IReadOnlyList SecondFaGroups { get; init; } + [ConfigParameter("second-fa-bypass-groups")] public IReadOnlyList SecondFaBypassGroups { get; init; } + [ConfigParameter("load-nested-groups")] public bool LoadNestedGroups { get; init; } + [ConfigParameter("nested-groups-base-dn")] public IReadOnlyList NestedGroupsBaseDns { get; init; } + [ConfigParameter("authentication-cache-groups")] public IReadOnlyList AuthenticationCacheGroups { get; init; } + [ConfigParameter("phone-attributes")] public IReadOnlyList PhoneAttributes { get; init; } + [ConfigParameter("identity-attribute")] public string IdentityAttribute { get; init; } + [ConfigParameter("requires-upn")] public bool RequiresUpn { get; init; } + [ConfigParameter("enable-trusted-domains")] public bool TrustedDomainsEnabled { get; init; } + [ConfigParameter("enable-alternative-suffixes")] public bool AlternativeSuffixesEnabled { get; init; } + [ConfigParameter("included-domains")] public IReadOnlyList IncludedDomains { get; init; } + [ConfigParameter("excluded-domains")] public IReadOnlyList ExcludedDomains { get; init; } + [ConfigParameter("included-suffixes")] public IReadOnlyList IncludedSuffixes { get; init; } + [ConfigParameter("excluded-suffixes")] public IReadOnlyList ExcludedSuffixes { get; init; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs index 6ec400d2..4f5f81e4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs @@ -1,24 +1,42 @@ using System.Net; +using Multifactor.Radius.Adapter.v2.Shared.Attributes; namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public class RootConfiguration -{ - public required IReadOnlyList MultifactorApiUrls { get; set; } +{ + [ConfigParameter("multifactor-api-url")] + public IReadOnlyList MultifactorApiUrls { get; set; } + [ConfigParameter("multifactor-api-proxy")] public string? MultifactorApiProxy { get; set; } + [ConfigParameter("multifactor-api-timeout")] public TimeSpan MultifactorApiTimeout { get; set; } - public required IPEndPoint? AdapterServerEndpoint { get; set; } + [ConfigParameter("adapter-server-endpoint")] + public IPEndPoint? AdapterServerEndpoint { get; set; } + [ConfigParameter("logging-level")] + public string LoggingLevel { get; set; } - public string LoggingFormat { get; set; } = string.Empty; - public bool SyslogUseTls { get; set; } = false; - public string SyslogServer { get; set; } = string.Empty; - public string SyslogFormat { get; set; } = string.Empty; - public string SyslogFacility { get; set; } = string.Empty; - public string SyslogAppName { get; set; } = "multifactor-radius"; - public string SyslogFramer { get; set; } = string.Empty; - public string SyslogOutputTemplate { get; set; } = string.Empty; + [ConfigParameter("logging-format")] + public string LoggingFormat { get; set; } + [ConfigParameter("syslog-use-tls")] + public bool SyslogUseTls { get; set; } + [ConfigParameter("syslog-server")] + public string SyslogServer { get; set; } + [ConfigParameter("syslog-format")] + public string SyslogFormat { get; set; } + [ConfigParameter("syslog-facility")] + public string SyslogFacility { get; set; } + [ConfigParameter("syslog-app-name", "multifactor-radius")] + public string SyslogAppName { get; set; } + [ConfigParameter("syslog-framer")] + public string SyslogFramer { get; set; } + [ConfigParameter("syslog-output-template")] + public string SyslogOutputTemplate { get; set; } - public string ConsoleLogOutputTemplate { get; set; } = string.Empty; - public string FileLogOutputTemplate { get; set; } = string.Empty; - public int LogFileMaxSizeBytes { get; set; } = 1073741824; + [ConfigParameter("console-log-output-template")] + public string ConsoleLogOutputTemplate { get; set; } + [ConfigParameter("file-log-output-template")] + public string FileLogOutputTemplate { get; set; } + [ConfigParameter("log-file-max-size-bytes", 1073741824)] + public int LogFileMaxSizeBytes { get; set; } } \ 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 index 3ce4e674..1915a8eb 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -6,6 +6,6 @@ public class ServiceConfiguration { public required RootConfiguration RootConfiguration { get; set; } public required IReadOnlyList ClientsConfigurations { get; set; } - public ClientConfiguration GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.First(config => config.MultifactorNasIdentifier == nasIdentifier); - public ClientConfiguration GetClientConfiguration(IPAddress ip) => ClientsConfigurations.First(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); + public ClientConfiguration GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientNasIdentifier == nasIdentifier); + public ClientConfiguration GetClientConfiguration(IPAddress ip) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs index 51123870..a8d2b47e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs @@ -1,10 +1,10 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; +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.Steps; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs deleted file mode 100644 index 8948519b..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeStatus.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; - -public enum ChallengeStatus -{ - Reject = 0, - InProcess, - Accept -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs deleted file mode 100644 index 3079c831..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/ChallengeType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; - -public enum ChallengeType -{ - None = 0, - SecondFactor, - PasswordChange -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs deleted file mode 100644 index 03a57b5b..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessorProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; - -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; - -public interface IFirstFactorProcessorProvider -{ - IFirstFactorProcessor GetProcessor(AuthenticationSource authSource); -} diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs index 46f98528..1ee565f3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs @@ -1,8 +1,5 @@ -using Multifactor.Core.Ldap.Connection; -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.Models; namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs index 65cb6c64..9161297e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs @@ -12,7 +12,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() ?? []; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs index 9a0cd942..d6994bf4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestResponse.cs @@ -1,4 +1,4 @@ -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; 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 index c21c2e74..a9926ef9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs @@ -1,4 +1,4 @@ -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs index 350492da..f3b71adf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs @@ -1,29 +1,31 @@ using System.Net; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Attributes; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Configuration.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.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor; -//TODO separate creation and sending api context -public class MultifactorApiService +public sealed class MultifactorApiService { private readonly IMultifactorApi _api; private readonly IAuthenticatedClientCache _authenticatedClientCache; private readonly ILogger _logger; - public MultifactorApiService(IMultifactorApi api, IAuthenticatedClientCache authenticatedClientCache, 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; @@ -32,17 +34,20 @@ public MultifactorApiService(IMultifactorApi api, IAuthenticatedClientCache auth public async Task CreateSecondFactorRequestAsync(RadiusPipelineContext context, bool cacheEnabled) { ArgumentNullException.ThrowIfNull(context, nameof(context)); - var secondFactorIdentity = GetSecondFactorIdentity(context); - if (string.IsNullOrWhiteSpace(secondFactorIdentity)) + + 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); } + // return new SecondFactorResponse(AuthenticationStatus.Bypass); - var personalData = GetPersonalData(context); - - //try to get authenticated client to bypass second factor if configured - if (_authenticatedClientCache.TryHitCache(personalData.CallingStationId, personalData.Identity, context.ClientConfiguration.Name, context.ClientConfiguration.AuthenticationCacheLifetime)) + 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}", @@ -53,27 +58,16 @@ public async Task CreateSecondFactorRequestAsync(RadiusPip return new SecondFactorResponse(AuthenticationStatus.Bypass); } - ApplyPrivacyMode(personalData, context.ClientConfiguration.PrivacyMode, context.ClientConfiguration.PrivacyFields); - - SecondFactorResponse cloudResponse; + ApplyPrivacyMode(ref personalData, context.ClientConfiguration.Privacy.PrivacyMode, context.ClientConfiguration.Privacy.PrivacyFields); - // TODO move to method try { - var phone = context.LdapProfile?.Attributes - .Where(x => context.LdapConfiguration.PhoneAttributes.Contains(x.Name.Value)) - .Select(x => x.GetNotEmptyValues().FirstOrDefault()) - .FirstOrDefault(); - var dto = new AccessRequestQuery - { - Identity = personalData.Identity, - Name = context.LdapProfile.DisplayName, - Email = context.LdapProfile.Email, - Phone = string.IsNullOrWhiteSpace(phone) ? context.LdapProfile?.Phone : phone, - CalledStationId = context.RequestPacket.CalledStationIdAttribute, - CallingStationId = personalData.CallingStationId - }; - var response = await _api.CreateAccessRequest(dto); + 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) @@ -98,25 +92,23 @@ public async Task CreateSecondFactorRequestAsync(RadiusPip } LogGrantedInfo(personalData.Identity, response, context.RequestPacket.CallingStationIdAttribute); - _authenticatedClientCache.SetCache(personalData.CallingStationId, personalData.Identity, context.ClientConfiguration.Name, context.ClientConfiguration.AuthenticationCacheLifetime); + _authenticatedClientCache.SetCache( + personalData.CallingStationId, + personalData.Identity, + context.ClientConfiguration.Name, + context.ClientConfiguration.AuthenticationCacheLifetime); return mfResponse; } catch (MultifactorApiUnreachableException apiEx) { - cloudResponse = ProcessMfException(apiEx, personalData.Identity, + return ProcessMfException(apiEx, personalData.Identity, context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, context.RequestPacket.RemoteEndpoint); } catch (Exception ex) { - cloudResponse = ProcessException(ex, personalData.Identity, context.RequestPacket.RemoteEndpoint); + return ProcessException(ex, personalData.Identity, context.RequestPacket.RemoteEndpoint); } - - - if (cloudResponse.Code == AuthenticationStatus.Bypass) - _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", personalData.Identity, context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); - - return cloudResponse; } public async Task SendChallengeAsync(RadiusPipelineContext context, bool cacheEnabled, string requestId, string answer) @@ -125,8 +117,7 @@ public async Task SendChallengeAsync(RadiusPipelineContext ArgumentException.ThrowIfNullOrWhiteSpace(requestId, nameof(requestId)); ArgumentException.ThrowIfNullOrWhiteSpace(answer, nameof(answer)); - var identity = GetSecondFactorIdentity(context.LdapConfiguration.IdentityAttribute, context.RequestPacket.UserName, context.LdapProfile?.Attributes ?? []); - + var identity = RequestDataExtractor.GetSecondFactorIdentity(context); if (string.IsNullOrWhiteSpace(identity)) throw new InvalidOperationException("The identity is empty."); @@ -138,12 +129,15 @@ public async Task SendChallengeAsync(RadiusPipelineContext }; var callingStationIdAttr = context.RequestPacket.CallingStationIdAttribute; - var callingStationId = GetCallingStationId(callingStationIdAttr, context.RequestPacket.RemoteEndpoint); - SecondFactorResponse cloudResponse; + var callingStationId = RequestDataExtractor.GetCallingStationId(callingStationIdAttr, context.RequestPacket.RemoteEndpoint); try { - var response = await _api.SendChallengeAsync(dto); + 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); @@ -155,24 +149,24 @@ public async Task SendChallengeAsync(RadiusPipelineContext } LogGrantedInfo(identity, response, callingStationId); - _authenticatedClientCache.SetCache(callingStationId, identity, context.ClientConfiguration.Name, context.ClientConfiguration.AuthenticationCacheLifetime); + _authenticatedClientCache.SetCache( + callingStationId, + identity, + context.ClientConfiguration.Name, + context.ClientConfiguration.AuthenticationCacheLifetime); return mfResponse; } catch (MultifactorApiUnreachableException apiEx) { - cloudResponse = ProcessMfException(apiEx, identity, context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, context.RequestPacket.RemoteEndpoint); + return ProcessMfException(apiEx, identity, + context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, + context.RequestPacket.RemoteEndpoint); } catch (Exception ex) { - cloudResponse = ProcessException(ex, identity, context.RequestPacket.RemoteEndpoint); + return ProcessException(ex, identity, context.RequestPacket.RemoteEndpoint); } - - if (cloudResponse.Code == AuthenticationStatus.Bypass) - _logger.LogWarning("Bypass second factor for user '{user:l}' from {host:l}:{port}", identity, - context.RequestPacket.RemoteEndpoint.Address, context.RequestPacket.RemoteEndpoint.Port); - - return cloudResponse; } private AuthenticationStatus ConvertToAuthCode(AccessRequestResponse? multifactorAccessRequest) @@ -227,87 +221,22 @@ private void LogGrantedInfo(string identity, AccessRequestResponse? response, st response?.AuthenticatorId); } - 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 string? GetSecondFactorIdentity(RadiusPipelineContext context) - { - if (string.IsNullOrWhiteSpace(context.LdapConfiguration.IdentityAttribute)) - return context.RequestPacket.UserName; - - return context.LdapProfile?.Attributes - .FirstOrDefault(x => x.Name == context.LdapConfiguration.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(RadiusPipelineContext context) + private AccessRequestQuery CreateAccessRequestQuery(PersonalData personalData, RadiusPipelineContext context) { - var secondFactorIdentity = GetSecondFactorIdentity(context); - var callingStationId = context.RequestPacket.CallingStationIdAttribute; - - var callingStationIdForApiRequest = GetCallingStationId(callingStationId, context.RequestPacket.RemoteEndpoint); - - var phone = context.LdapProfile?.Attributes - .Where(x => context.LdapProfile.Phone.Contains(x.Name.Value)) - .Select(x => x.GetNotEmptyValues().FirstOrDefault()) - .FirstOrDefault(); - - var personalData = new PersonalData + var phone = RequestDataExtractor.GetUserPhone(context); + + return new AccessRequestQuery { - Identity = secondFactorIdentity!, - DisplayName = context.LdapProfile?.DisplayName, - Email = context.LdapProfile?.Email, - Phone = string.IsNullOrWhiteSpace(phone) ? context.LdapProfile?.Phone : phone, - CalledStationId = context.RequestPacket.CalledStationIdAttribute, - CallingStationId = callingStationIdForApiRequest + Identity = personalData.Identity, + Name = personalData.DisplayName, + Email = personalData.Email, + Phone = string.IsNullOrWhiteSpace(phone) ? personalData.Phone : phone, + CalledStationId = personalData.CalledStationId, + CallingStationId = personalData.CallingStationId }; - - 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 void ApplyPrivacyMode(PersonalData pd, PrivacyMode mode, string[] privacyFields) + private static void ApplyPrivacyMode(ref PersonalData pd, PrivacyMode mode, string[] privacyFields) { switch (mode) { @@ -333,12 +262,15 @@ private void ApplyPrivacyMode(PersonalData pd, PrivacyMode mode, string[] privac pd.CallingStationId = ""; pd.CalledStationId = null; - break; } } - private SecondFactorResponse ProcessMfException(MultifactorApiUnreachableException apiEx, string identity, bool bypassSecondFactorWhenApiUnreachable, IPEndPoint remoteEndpoint) + private SecondFactorResponse 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}", @@ -359,7 +291,8 @@ private SecondFactorResponse ProcessMfException(MultifactorApiUnreachableExcepti 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}", + _logger.LogError(ex, + "Error occured while requesting API for user '{user:l}' from {host:l}:{port}, {msg:l}", identity, remoteEndpoint.Address, remoteEndpoint.Port, @@ -369,5 +302,6 @@ private SecondFactorResponse ProcessException(Exception ex, string identity, IPE return new SecondFactorResponse(code); } - private static bool ShouldCacheResponse(bool apiResponseCacheEnabled, AuthenticationStatus responseCode, AccessRequestResponse? response) => apiResponseCacheEnabled && responseCode == AuthenticationStatus.Accept && !(response?.Bypassed ?? false); + 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 index f4b3f7b7..869e1674 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Ports/IMultifactorApi.cs @@ -4,6 +4,6 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; public interface IMultifactorApi { - Task CreateAccessRequest(AccessRequestQuery query, CancellationToken cancellationToken = default); - Task SendChallengeAsync(ChallengeRequestQuery query, CancellationToken cancellationToken = default); + 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..fff43416 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs @@ -0,0 +1,82 @@ +using System.Net; +using Multifactor.Core.Ldap.Attributes; +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 ?? string.Empty, + 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) + { + if (string.IsNullOrWhiteSpace(context.LdapConfiguration?.IdentityAttribute)) + return context.RequestPacket.UserName; + + return GetAttributeValue(context.LdapProfile?.Attributes, context.LdapConfiguration.IdentityAttribute); + } + + 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)) + { + foreach (var value in attribute.GetNotEmptyValues()) + { + if (!string.IsNullOrEmpty(value)) + return value; + } + } + } + + return context.LdapProfile.Phone; + } + + private static string? GetAttributeValue(IReadOnlyCollection? attributes, string attributeName) + { + if (attributes == null) return null; + + foreach (var attr in attributes) + { + if (attr.Name == attributeName) + { + foreach (var value in attr.Values) + { + if (!string.IsNullOrWhiteSpace(value)) + return value; + } + } + } + + return null; + } + + 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.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs similarity index 80% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs index 803d4507..5618fc7a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs @@ -1,6 +1,6 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public class ChallengeProcessorProvider : IChallengeProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs similarity index 94% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs index a9e7a5f5..387e871e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/ChangePasswordChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +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.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Security; +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.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public class ChangePasswordChallengeProcessor : IChallengeProcessor { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs similarity index 73% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs index 77925ef9..38e6dbf4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs @@ -1,7 +1,7 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +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.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public interface IChallengeProcessor { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs similarity index 54% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs index 3101d92a..451807f9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/IChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs @@ -1,6 +1,6 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public interface IChallengeProcessorProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/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.Application/Features/AccessChallenge/Models/ChallengeIdentifier.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeIdentifier.cs index 0abb727c..6a8d6cf4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/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.Application.Features.AccessChallenge.Models; +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/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs new file mode 100644 index 00000000..88954dff --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; + +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/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs new file mode 100644 index 00000000..39ee904f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; + +public enum ChallengeType +{ + None = 0, + SecondFactor, + PasswordChange +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs similarity index 73% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs index 0f53c39c..84a1396e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PasswordChangeCache.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PasswordChangeCache.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; public class PasswordChangeCache { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/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.Application/Features/AccessChallenge/Models/PersonalData.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PersonalData.cs index 66a56e98..9842632a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/Models/PersonalData.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/PersonalData.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; public class PersonalData { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs similarity index 74% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs index bb0465ba..3deb82f5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/AccessChallenge/SecondFactorChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs @@ -1,33 +1,61 @@ -using System.Collections.Concurrent; using System.Text; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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.Models; +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; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; -namespace Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; public class SecondFactorChallengeProcessor : IChallengeProcessor { - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary _challengeContexts = new(); + 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) + 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) @@ -36,17 +64,32 @@ public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) ArgumentException.ThrowIfNullOrWhiteSpace(context.ResponseInformation.State); var id = new ChallengeIdentifier(context.ClientConfiguration.Name, context.ResponseInformation.State); - if (_challengeContexts.TryAdd(id, context)) + var key = CreateCacheKey(id); + + try { - _logger.LogInformation("Challenge {State:l} was added for message id={id}", id.RequestId, context.RequestPacket.Identifier); + _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; } - - _logger.LogError("Unable to cache request id={id} for the '{cfg:l}' configuration", context.RequestPacket.Identifier, context.ClientConfiguration.Name); - return ChallengeIdentifier.Empty; + 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) => _challengeContexts.ContainsKey(identifier); + public bool HasChallengeContext(ChallengeIdentifier identifier) + { + var key = CreateCacheKey(identifier); + return _memoryCache.TryGetValue(key, out _); + } public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) { @@ -71,11 +114,15 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide return ProcessResponse(context, challengeContext, response, identifier); } - private RadiusPipelineContext? GetChallengeContext(ChallengeIdentifier identifier) { - if (_challengeContexts.TryGetValue(identifier, out RadiusPipelineContext? request)) - return request; + 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; @@ -83,9 +130,16 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide private void RemoveChallengeContext(ChallengeIdentifier identifier) { - _challengeContexts.TryRemove(identifier, out _); + 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; @@ -215,6 +269,7 @@ private bool ShouldCacheResponse(RadiusPipelineContext context) 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs similarity index 72% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs index 5445f56c..e90c095e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatter.cs @@ -1,8 +1,7 @@ using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class ActiveDirectoryFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs similarity index 84% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs index 713480fa..b2547b34 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/FreeIpaFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatter.cs @@ -2,9 +2,8 @@ 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class FreeIpaFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs similarity index 64% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs index 60950491..9a55a5b4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatter.cs @@ -1,8 +1,7 @@ using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public interface ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/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.Application/Features/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/ILdapBindNameFormatterProvider.cs index 247a9f16..d517b5c2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/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.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public interface ILdapBindNameFormatterProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs similarity index 84% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs index 758e6575..af4c4d58 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProvider.cs @@ -1,6 +1,6 @@ using Multifactor.Core.Ldap.Schema; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class LdapBindNameFormatterProvider : ILdapBindNameFormatterProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs similarity index 84% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs index 7fa2a307..7fd56119 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatter.cs @@ -2,9 +2,8 @@ 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class MultiDirectoryFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs similarity index 84% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs index 06404a35..8dd857b7 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/OpenLdapFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatter.cs @@ -2,9 +2,8 @@ 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class OpenLdapFormatter: ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs similarity index 84% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs index e306a6db..331a6b9f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/BindNameFormat/SambaFormatter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/BindNameFormat/SambaFormatter.cs @@ -2,9 +2,8 @@ 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor.BindNameFormat; public class SambaFormatter : ILdapBindNameFormatter { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/FirstFactorProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/FirstFactorProcessorProvider.cs similarity index 74% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/FirstFactorProcessorProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/FirstFactorProcessorProvider.cs index f6f1cb3d..1b07019e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/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.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Application.Features.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/FirstFactor/IFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs similarity index 66% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs index 82714a43..5de57ce9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/IFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessor.cs @@ -1,7 +1,7 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public interface IFirstFactorProcessor { 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..d753117f --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/IFirstFactorProcessorProvider.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.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/FirstFactor/LdapFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs similarity index 91% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs index d5a6ab4c..5563bf82 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/LdapFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs @@ -1,16 +1,14 @@ using System.DirectoryServices.Protocols; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Configuration; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; +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.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class LdapFirstFactorProcessor : IFirstFactorProcessor { @@ -104,9 +102,8 @@ private bool ValidateUserCredentials( Password = password, BindTimeoutInSeconds = serverConfig.BindTimeoutSeconds }; - _ldapAdapter.CheckConnecion(request); - return true; + return _ldapAdapter.CheckConnecion(request); } catch (Exception ex) { @@ -124,18 +121,18 @@ private bool ValidateUserCredentials( return false; } - private void Reject(RadiusPipelineContext context) + private static void Reject(RadiusPipelineContext context) { context.FirstFactorStatus = AuthenticationStatus.Reject; } - private void Accept(RadiusPipelineContext context) + private static void Accept(RadiusPipelineContext context) { context.FirstFactorStatus = AuthenticationStatus.Accept; } - private bool CheckLdapException(LdapException exception, out string reasonText) + private static bool CheckLdapException(LdapException exception, out string reasonText) { if (string.IsNullOrWhiteSpace(exception.ServerErrorMessage)) { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/NoneFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/NoneFirstFactorProcessor.cs similarity index 77% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/NoneFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/NoneFirstFactorProcessor.cs index b648d496..54276446 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/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.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class NoneFirstFactorProcessor : IFirstFactorProcessor { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/RadiusFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs similarity index 91% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/RadiusFirstFactorProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs index 0e709945..8c1bf3af 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/FirstFactor/RadiusFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/RadiusFirstFactorProcessor.cs @@ -1,14 +1,13 @@ using System.Net; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.LangFeatures; +using Multifactor.Radius.Adapter.v2.Application.Configuration.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; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; -namespace Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.FirstFactor; public class RadiusFirstFactorProcessor : IFirstFactorProcessor { @@ -27,10 +26,10 @@ public RadiusFirstFactorProcessor(IRadiusPacketService radiusPacketService, IRad 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)) { @@ -113,7 +112,7 @@ private static RadiusPacket PreparePacket(RadiusPacket radiusPacket, string user 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/IPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs index 09019846..50b5c958 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs @@ -1,6 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; public interface IPipelineProvider { - IRadiusPipeline? GetPipeline(string key); + public IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs index 83ccce78..93fd2679 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs @@ -1,6 +1,6 @@ using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; public interface IRadiusPipelineFactory { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/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.Application/Models/Enum/AuthenticationStatus.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/AuthenticationStatus.cs index 94099553..dc9be44f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/AuthenticationStatus.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/Enum/AuthenticationStatus.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; public enum AuthenticationStatus { 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 index 7e03c052..c8baa337 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs @@ -1,11 +1,9 @@ using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Configuration; 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; 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 index d942627c..a6fb4d1a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs @@ -5,26 +5,26 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; public class UserIdentity { - public string Identity { get; private set; } - public UserIdentityFormat Format { get; private set; } + public string Identity { get; init; } + public UserIdentityFormat Format { get; init; } public UserIdentity(string identity) { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); Identity = identity; Format = GetIdentityTypeByIdentity(identity); } public UserIdentity(string identity, UserIdentityFormat format) { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); Identity = identity; Format = format; } - private UserIdentityFormat GetIdentityTypeByIdentity(string identity) + private static UserIdentityFormat GetIdentityTypeByIdentity(string identity) { - Throw.IfNullOrWhiteSpace(identity, nameof(identity)); + ArgumentException.ThrowIfNullOrWhiteSpace(identity, nameof(identity)); var id = identity.ToLower(); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs index 070fde25..c659ebc9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs @@ -3,7 +3,7 @@ //https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md using System.Text.RegularExpressions; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs index c14cd1bd..5b049f10 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.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.Models; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; 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 index 5f9da66e..2821d3b9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs @@ -1,10 +1,8 @@ using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap.Name; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; @@ -32,10 +30,9 @@ public Task ExecuteAsync(RadiusPipelineContext context) return Task.CompletedTask; ArgumentNullException.ThrowIfNull(context.LdapProfile, nameof(context.LdapProfile)); - - var request = MembershipRequest.FromContext(context, context.LdapConfiguration.AccessGroups); - //TODO логику из стараго сервиса сюда и оставить только вызов адаптера - var isMember = _ldapAdapter.IsMemberOf(request); + 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); } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs index 5952b3c6..9f254cda 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; +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.FirstFactor; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs index e9085d2d..3299d932 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/IpWhiteListStep.cs @@ -1,7 +1,7 @@ using System.Net; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; 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 index 5edde569..0786e515 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs @@ -54,14 +54,9 @@ public Task ExecuteAsync(RadiusPipelineContext context) return schema; var expirationDate = DateTimeOffset.Now.AddHours(LdapSchemaCacheLifeTimeInHours); - SaveToCache(cacheKey, schema, expirationDate); + _cache.Set(cacheKey, schema, expirationDate); _logger.LogDebug("LDAP schema for '{domain}' is saved in cache till '{expirationDate}'.", cacheKey, expirationDate.ToString()); return schema; } - - 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.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs index 1db316a4..90835ed9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; @@ -18,7 +19,7 @@ public Task ExecuteAsync(RadiusPipelineContext context) _logger.LogDebug("'{name}' started", nameof(PreAuthCheckStep)); switch (context.ClientConfiguration.PreAuthenticationMethod) { - case PreAuthMode.Otp when context.Passphrase.Otp == null: + case PreAuthMode.Otp when 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, diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs index 58d49e54..b4ba9974 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthPostCheck.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs index 93cc0795..95c2a1fc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs @@ -5,8 +5,6 @@ using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.Ports; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; @@ -97,7 +95,7 @@ public Task ExecuteAsync(RadiusPipelineContext context) return profile; } - private IEnumerable GetAttributes(RadiusPipelineContext context) + private static IEnumerable GetAttributes(RadiusPipelineContext context) { var attributes = new List() { new("memberOf"), new("userPrincipalName"), new("phone"), new("mail"), new("displayName"), new("email") }; if (!string.IsNullOrWhiteSpace(context.LdapConfiguration!.IdentityAttribute)) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs index 5f545b3d..0d6c758c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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.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.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; @@ -66,7 +66,7 @@ private bool ShouldCallSecondFactor(RadiusPipelineContext context) return false; } - private bool ShouldBypassByRequest(RadiusPipelineContext context) + private static bool ShouldBypassByRequest(RadiusPipelineContext context) { return context.RequestPacket.IsVendorAclRequest && context.ClientConfiguration.FirstFactorAuthenticationSource == AuthenticationSource.Radius; } @@ -83,7 +83,7 @@ private bool ShouldBypassByGroups(RadiusPipelineContext context) if (serverConfig.SecondFaBypassGroups.Any()) { var request = MembershipRequest.FromContext(context, serverConfig.SecondFaBypassGroups); - bypassMember = _ldapAdapter.IsMemberOf(request); + bypassMember = context.LdapProfile.MemberOf.Intersect(serverConfig.SecondFaBypassGroups).Any() || _ldapAdapter.IsMemberOf(request); } if (bypassMember is true) @@ -96,7 +96,7 @@ private bool ShouldBypassByGroups(RadiusPipelineContext context) if (serverConfig.SecondFaGroups.Any()) { var request = MembershipRequest.FromContext(context, serverConfig.SecondFaGroups); - secondFactorMember = _ldapAdapter.IsMemberOf(request); + 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); } @@ -122,7 +122,7 @@ private bool ShouldCacheResponse(RadiusPipelineContext context) } var request = MembershipRequest.FromContext(context, context.LdapConfiguration.AuthenticationCacheGroups); - var isMember = _ldapAdapter.IsMemberOf(request); + 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) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs index 4840b79f..b8dac41b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Logging; 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; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; 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 index 8fd93807..a76b9b74 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs @@ -1,14 +1,8 @@ -using System.DirectoryServices.Protocols; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Radius.Adapter.v2.Application.Configuration; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; @@ -140,7 +134,7 @@ private bool UnsupportedAccountType(RadiusPipelineContext context) return true; } - private bool AcceptedRequest(RadiusPipelineContext context) + private static bool AcceptedRequest(RadiusPipelineContext context) { return context.FirstFactorStatus is AuthenticationStatus.Accept or AuthenticationStatus.Bypass diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs index 1ee404b8..e83966d7 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; 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.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs deleted file mode 100644 index 02be99f2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/RadiusProcessingException.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; - -public class RadiusProcessingException : Exception -{ - public string ClientName { get; } - - public RadiusProcessingException(string message) : base(message) { } - - public RadiusProcessingException(string message, Exception innerException) - : base(message, innerException) { } - - public RadiusProcessingException(string message, string clientName) - : base(message) - { - ClientName = clientName; - } - - public RadiusProcessingException(string message, string clientName, Exception innerException) - : base(message, innerException) - { - ClientName = clientName; - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs index be634056..1b4166bd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs @@ -2,6 +2,7 @@ using System.Text; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; using Multifactor.Radius.Adapter.v2.Shared; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs index 99f05fca..331b28ac 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs @@ -20,7 +20,7 @@ public class RadiusPacketHeader public RadiusPacketHeader(PacketCode code, byte identifier, byte[] authenticator) { - Throw.IfNull(authenticator, nameof(authenticator)); + ArgumentNullException.ThrowIfNull(authenticator, nameof(authenticator)); Code = code; Identifier = identifier; 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 index 126e6724..10135015 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs @@ -1,9 +1,8 @@ using System.Net; using Multifactor.Core.Ldap.Attributes; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs similarity index 80% rename from src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs index dea2a2af..b9f23413 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Ports/IUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Sockets; -namespace Multifactor.Radius.Adapter.v2.Application.Ports; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; public interface IUdpClient : IDisposable { 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 index 6d7098fa..29df2fdd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs @@ -66,7 +66,6 @@ private async Task TryProcessWithLdapServers(ClientConfiguration clientConfigura _logger.LogWarning(ex, "Failed to process with LDAP server {ConnectionString} for client {ClientName}", serverConfig.ConnectionString, clientConfiguration.Name); - // Продолжаем пробовать следующий сервер } } @@ -74,7 +73,7 @@ private async Task TryProcessWithLdapServers(ClientConfiguration clientConfigura { _logger.LogError(lastException, "All LDAP servers failed for client {ClientName}", clientConfiguration.Name); - throw new RadiusProcessingException( + throw new Exception( $"All LDAP servers failed for client '{clientConfiguration.Name}'", lastException); } @@ -85,7 +84,6 @@ private async Task ExecutePipeline( RadiusPacket requestPacket, LdapServerConfiguration? ldapServerConfiguration = null) { - // Логирование if (ldapServerConfiguration != null) { _logger.LogDebug( @@ -100,13 +98,12 @@ private async Task ExecutePipeline( } var context = CreatePipelineContext(clientConfiguration, requestPacket, ldapServerConfiguration); - var pipeline = GetPipeline(clientConfiguration.Name); + var pipeline = GetPipeline(clientConfiguration); try { await pipeline.ExecuteAsync(context); - // Отправка ответа var responseRequest = SendAdapterResponseRequest.FromContext(context); await _responseSender.SendResponse(responseRequest); @@ -120,18 +117,15 @@ private async Task ExecutePipeline( } catch (Exception ex) { - // Логируем как Warning, т.к. это временная ошибка выполнения _logger.LogWarning(ex, "Pipeline execution failed for client {ClientName}{ServerInfo}", clientConfiguration.Name, ldapServerConfiguration != null ? $" with LDAP server {ldapServerConfiguration.ConnectionString}" : ""); - - // Пробрасываем дальше для TryProcessWithLdapServers throw; } } - private RadiusPipelineContext CreatePipelineContext( + private static RadiusPipelineContext CreatePipelineContext( ClientConfiguration clientConfiguration, RadiusPacket requestPacket, LdapServerConfiguration? ldapServerConfiguration = null) @@ -148,32 +142,27 @@ private RadiusPipelineContext CreatePipelineContext( return context; } - private IRadiusPipeline GetPipeline(string clientConfigurationName) + private IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) { - var pipeline = _pipelineProvider.GetPipeline(clientConfigurationName); + var pipeline = _pipelineProvider.GetPipeline(clientConfiguration); if (pipeline is null) { throw new PipelineNotFoundException( - $"No pipeline found for client '{clientConfigurationName}'. " + + $"No pipeline found for client '{clientConfiguration.Name}'. " + "Check adapter configuration and restart the adapter.", - clientConfigurationName); + clientConfiguration.Name); } return pipeline; } - private bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, ClientConfiguration clientConfiguration) + private static bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, ClientConfiguration clientConfiguration) { - // Если нет LDAP серверов if (clientConfiguration.LdapServers.Count <= 0) return true; - // Если пакет не является AccessRequest if (requestPacket.Code != PacketCode.AccessRequest) return true; - // Дополнительные условия можно добавить здесь - // Например, если есть определенные атрибуты или флаги - return false; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs similarity index 95% rename from src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs index 950aa7ac..f5c79195 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Security/ProtectionService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using System.Text; -namespace Multifactor.Radius.Adapter.v2.Application.Security; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Security; public static class ProtectionService { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs similarity index 97% rename from src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs index 3f450b70..83466169 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Security/RadiusPasswordProtector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs @@ -2,7 +2,7 @@ using System.Text; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Security +namespace Multifactor.Radius.Adapter.v2.Application.Features.Security { public static class RadiusPasswordProtector { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs deleted file mode 100644 index b98c68a8..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Models/Enum/RequestStatus.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Models.Enum; - -public enum RequestStatus -{ - AwaitingAuthentication, - Granted, - Denied -} \ No newline at end of file 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 index 740eec55..c295aadf 100644 --- 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 @@ -8,6 +8,7 @@ + diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs index 2f63b4f9..b3a0a864 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs @@ -1,7 +1,4 @@ -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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs index fafccd1d..1b556ad0 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs @@ -7,18 +7,10 @@ using Multifactor.Core.Ldap.Connection; using Multifactor.Core.Ldap.Name; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; -using Multifactor.Radius.Adapter.v2.Application.Models; -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.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.Infrastructure.Radius.Sender; using Multifactor.Radius.Adapter.v2.Server; using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; @@ -102,7 +94,7 @@ protected IRadiusPacket SendPacketAsync(IRadiusPacket? radiusPacket, SharedSecre return parsed; } - protected RadiusPacket CreateRadiusPacket(PacketCode packetCode, byte identifier = 0) + protected static RadiusPacket CreateRadiusPacket(PacketCode packetCode, byte identifier = 0) { RadiusPacket packet; switch (packetCode) @@ -161,7 +153,7 @@ protected IClientConfiguration CreateClientConfiguration(RadiusAdapterConfigurat return _clientConfigurationFactory.CreateConfig("e2e", configuration, serviceConfig!); } - private ModifyRequest BuildModifyRequest( + private static ModifyRequest BuildModifyRequest( DistinguishedName dn, string attributeName, object attributeValue) diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs index ec2d14f8..9bba64a9 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs @@ -1,13 +1,9 @@ using System.Net; using Microsoft.Extensions.Logging.Abstractions; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Application.Models; -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.Infrastructure.Radius; namespace Multifactor.Radius.Adapter.v2.EndToEndTests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs index a7ea261c..29b3554e 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs @@ -1,8 +1,4 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs index 77aa22bf..6f336810 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs @@ -1,7 +1,4 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs index c844d34c..38f594ee 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs @@ -1,5 +1,3 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter; - namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; public class E2ERadiusConfiguration( diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs index 70acd784..30fd615d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs @@ -1,5 +1,3 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; - namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; public class RadiusConfigurationModel : RadiusConfigurationSource diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs index ae0a829b..ba53167c 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs @@ -1,4 +1,3 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs index ba9d8905..9e0adc88 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs @@ -1,13 +1,9 @@ using System.Text; using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs index 24bafb12..c0420c6d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs @@ -1,13 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs index 1a8132dc..96c40a4e 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs @@ -1,13 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs index 4b9e763d..aeee1ebc 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs @@ -2,13 +2,9 @@ using Microsoft.Extensions.Hosting; using Moq; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs index 870f357d..2d401274 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs index a19cebff..842e2177 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs index 02f1669a..1b1b97b5 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs index df97630a..e3dfbf6d 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs @@ -1,13 +1,9 @@ using System.Text; using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs index 7ed86490..2d723032 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs @@ -1,13 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs index a5145192..5f132ab5 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs index 402595fc..e07a95aa 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs index 1125cb45..b00d0836 100644 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Hosting; using Moq; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; 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; namespace Multifactor.Radius.Adapter.v2.EndToEndTests.Tests; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs index 13766c05..016cf2f3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -38,17 +38,18 @@ public LdapAdapter( public IReadOnlyList LoadUserGroups(LoadUserGroupRequest request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString, true), AuthType.Basic, request.UserName, request.Password, TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - var connection = _connectionFactory.CreateConnection(options); + using var connection = _connectionFactory.CreateConnection(options); 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) { var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), @@ -56,8 +57,18 @@ public IReadOnlyList LoadUserGroups(LoadUserGroupRequest request) request.UserName, request.Password, TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - var connection = _connectionFactory.CreateConnection(options); - var filter = GetFilter(request.UserIdentity, request.LdapSchema); + using var connection = _connectionFactory.CreateConnection(options); + + 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); 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); @@ -72,13 +83,14 @@ private string GetFilter(UserIdentity identity, ILdapSchema schema) return $"(&({objectClass}={classValue})({identityAttribute}={identity.Identity}))"; } - private string GetIdentityAttribute(UserIdentity identity, ILdapSchema schema) => identity.Format switch + 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(LoadSchemaRequest request) { @@ -97,13 +109,16 @@ public bool CheckConnecion(CheckConnectionRequest request) request.UserName, request.Password, TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - _connectionFactory.CreateConnection(options); - return true; + using var connection = _connectionFactory.CreateConnection(options); + return true; } #region IsMemberOf public bool IsMemberOf(MembershipRequest request) { + ArgumentNullException.ThrowIfNull(request); + if(request.TargetGroups == null || request.TargetGroups.Length == 0) + throw new InvalidOperationException(); var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), AuthType.Basic, request.UserName, @@ -128,24 +143,26 @@ private bool IsMemberOf(MembershipRequest request, ILdapConnection connection, D #region ChangeUserPassword public bool ChangeUserPassword(ChangeUserPasswordRequest request) { + ArgumentNullException.ThrowIfNull(request, nameof(request)); + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), AuthType.Basic, request.UserName, request.Password, TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - var connection = _connectionFactory.CreateConnection(options); + using var connection = _connectionFactory.CreateConnection(options); var changePasswordrequest = BuildPasswordChangeRequest(request.LdapSchema, request.DistinguishedName, request.NewPassword); var response = connection.SendRequest(changePasswordrequest); return response.ResultCode == ResultCode.Success; } - private ModifyRequest BuildPasswordChangeRequest(ILdapSchema ldapSchema, DistinguishedName userDn, string newPassword) + private static ModifyRequest BuildPasswordChangeRequest(ILdapSchema ldapSchema, DistinguishedName userDn, string newPassword) { var attributeName = ldapSchema.LdapServerImplementation == LdapImplementation.ActiveDirectory ? "unicodePwd" : "userpassword"; - var newPasswordAttribute = new DirectoryAttributeModification() + var newPasswordAttribute = new DirectoryAttributeModification { Name = attributeName, Operation = DirectoryAttributeOperation.Replace diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs similarity index 93% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs index edd8a002..6ca8551b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/ActivityContext.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/ActivityContext.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; public class ActivityContext { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs similarity index 96% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs index cb132185..707b56fe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/BasicAuthHeaderValue.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/BasicAuthHeaderValue.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http { /// /// Represents value (parameter) for a BASIC authentication header. diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs similarity index 88% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs index 5f16d4fa..37d384e3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/MfTraceIdHeaderSetter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/MfTraceIdHeaderSetter.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; public class MfTraceIdHeaderSetter : DelegatingHandler { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs similarity index 62% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs index 03064522..3eaf1dc6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Http/RoundRobinEndpointSelector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs @@ -1,44 +1,44 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; public interface IEndpointSelector { - Task GetNextEndpointAsync(); + Task GetNextEndpointAsync(); } public class RoundRobinEndpointSelector : IEndpointSelector { - private readonly List _endpoints; - private readonly ConcurrentDictionary _failedEndpoints; + private readonly IReadOnlyList _endpoints; + private readonly ConcurrentDictionary _failedEndpoints; private int _currentIndex = -1; - private readonly object _lock = new object(); + private readonly object _lock = new(); private readonly ILogger _logger; - public RoundRobinEndpointSelector(IConfiguration configuration, + public RoundRobinEndpointSelector(ServiceConfiguration configuration, ILogger logger) { - _endpoints = configuration.GetSection("ApiEndpoints") - .Get>() ?? []; - _failedEndpoints = new ConcurrentDictionary(); + _endpoints = configuration.RootConfiguration.MultifactorApiUrls; + _failedEndpoints = new ConcurrentDictionary(); _logger = logger; } - public async Task GetNextEndpointAsync() + public async Task GetNextEndpointAsync() { return await GetNextHealthyEndpointAsync(); } - private Task GetNextHealthyEndpointAsync() + private Task GetNextHealthyEndpointAsync() { if (_endpoints.Count == 0) throw new InvalidOperationException("No endpoints configured"); lock (_lock) { - foreach (var t in _endpoints) + foreach (var _ in _endpoints) { _currentIndex = (_currentIndex + 1) % _endpoints.Count; var endpoint = _endpoints[_currentIndex]; 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 93% 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 0bc446f8..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,6 +1,6 @@ using System.Net; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Http; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Http; public static class WebProxyFactory { 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 index 8677f390..975447df 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs @@ -1,5 +1,4 @@ using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; 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 index 96e0c95e..3102fa5e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/ChallengeRequestDto.cs @@ -1,5 +1,4 @@ using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs index 06ba9e76..291c9a1a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs @@ -1,10 +1,10 @@ +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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.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; @@ -22,13 +22,16 @@ public MultifactorApi(IHttpClientFactory clientFactory, _logger = logger; } - public async Task CreateAccessRequest(AccessRequestQuery query, CancellationToken cancellationToken) + public async Task CreateAccessRequest(AccessRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query, nameof(query)); var dto = AccessRequestDto.FromQuery(query); var client = _clientFactory.CreateClient(_clientName); + var headerValue = new BasicAuthHeaderValue(authData.ApiKey, authData.ApiSecret); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", headerValue.GetBase64()); var response = await client.PostAsJsonAsync("access/requests/ra", dto, cancellationToken: cancellationToken); + if (!response.IsSuccessStatusCode) { var errContent = await response.Content.ReadAsStringAsync(cancellationToken); @@ -36,17 +39,20 @@ public async Task CreateAccessRequest(AccessRequestQuery throw new Exception("Error while requesting access/requests/ra"); } response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(cancellationToken); + var content = await response.Content.ReadAsStringAsync(cancellationToken); //TODO add error cathcher + Console.WriteLine(content); var accessResponse = JsonSerializer.Deserialize>(content, _options); return accessResponse.Model; } - public async Task SendChallengeAsync(ChallengeRequestQuery query, CancellationToken cancellationToken) + public async Task SendChallengeAsync(ChallengeRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query, nameof(query)); var dto = ChallengeRequestDto.FromQuery(query); var client = _clientFactory.CreateClient(_clientName); + var headerValue = new BasicAuthHeaderValue(authData.ApiKey, authData.ApiSecret); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", headerValue.GetBase64()); var response = await client.PostAsJsonAsync("access/requests/ra/challenge", dto, cancellationToken: cancellationToken); if (!response.IsSuccessStatusCode) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs index d4a22f4e..2b74c8b3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs @@ -7,7 +7,7 @@ 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; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.PacketHandler; @@ -66,7 +66,7 @@ public async Task Handle(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 @@ -117,7 +117,7 @@ private bool IsRetransmission(RadiusPacket requestPacket) return false; } - private string CreateUniquePacketKey(RadiusPacket requestPacket) + private static string CreateUniquePacketKey(RadiusPacket requestPacket) { var base64Authenticator = requestPacket.Authenticator.Value.ToBase64(); return $"{requestPacket.Code:d}:{requestPacket.Identifier}:{requestPacket.RemoteEndpoint}:{requestPacket.UserName}:{base64Authenticator}"; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs index efb58699..a3e2c471 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Multifactor.Core.Ldap.LangFeatures; -using Multifactor.Radius.Adapter.v2.Application.Ports; +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Udp; @@ -22,29 +22,25 @@ public CustomUdpClient( _logger = logger; _options = options?.Value ?? new UdpClientOptions(); - _udpClient = new UdpClient(); ConfigureSocket(endPoint); - _logger?.LogInformation("UDP client initialized on {Endpoint}", endPoint); + _logger.LogInformation("UDP client initialized on {Endpoint}", endPoint); } private void ConfigureSocket(IPEndPoint endPoint) { + //TODO обоснования и дефолтные значения var socket = _udpClient.Client; socket.ReceiveBufferSize = _options.ReceiveBufferSize; socket.SendBufferSize = _options.SendBufferSize; - socket.ReceiveTimeout = _options.ReceiveTimeoutMs; socket.Ttl = _options.Ttl; socket.DontFragment = true; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(endPoint); - - socket.NoDelay = true; + // socket.NoDelay = true; } public async Task ReceiveAsync(CancellationToken cancellationToken = default) @@ -63,7 +59,7 @@ public async Task ReceiveAsync(CancellationToken cancellationT } catch (Exception ex) { - _logger?.LogError(ex, "UDP receive error"); + _logger.LogError(ex, "UDP receive error"); throw; } } @@ -82,7 +78,7 @@ public async Task SendAsync( if (bytesCount > 4096) { - _logger?.LogWarning("Attempted to send oversized RADIUS packet: {Size} bytes", bytesCount); + _logger.LogWarning("Attempted to send oversized RADIUS packet: {Size} bytes", bytesCount); throw new ArgumentException($"RADIUS packet too large: {bytesCount} bytes"); } @@ -93,17 +89,17 @@ public async Task SendAsync( } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.MessageSize) { - _logger?.LogWarning(ex, "Packet too large for MTU to {Endpoint}", endPoint); + _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); + _logger.LogWarning(ex, "Host unreachable: {Endpoint}", endPoint); throw; } catch (Exception ex) { - _logger?.LogError(ex, "UDP send error to {Endpoint}", endPoint); + _logger.LogError(ex, "UDP send error to {Endpoint}", endPoint); throw; } } @@ -123,11 +119,11 @@ public void Dispose() } } +//TODO Maybe move to config public class UdpClientOptions { public int ReceiveBufferSize { get; set; } = 64 * 1024; // 64KB public int SendBufferSize { get; set; } = 64 * 1024; // 64KB public int ReceiveTimeoutMs { get; set; } = 0; - public short Ttl { get; set; } = 32; - public bool EnableBroadcast { get; set; } = false; + public short Ttl { get; set; } = 30; } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs index 777a65e2..e4fd5bc3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs @@ -15,12 +15,12 @@ public AuthenticatedClient(string id, DateTime authenticatedAt) _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 index 8a418022..4a54f3a6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClientCache.cs @@ -1,66 +1,73 @@ -using System.Collections.Concurrent; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Cache.AuthenticatedClientCache; - public class AuthenticatedClientCache : IAuthenticatedClientCache +public class AuthenticatedClientCache : IAuthenticatedClientCache +{ + private readonly IMemoryCache _memoryCache; + private readonly ILogger _logger; + + public AuthenticatedClientCache(IMemoryCache memoryCache, ILogger logger) { - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary _authenticatedClients = new(); - private readonly ILogger _logger; + _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - public AuthenticatedClientCache(ILogger logger) - { - _logger = logger; - } + public bool TryHitCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime) + { + ArgumentException.ThrowIfNullOrWhiteSpace(clientName); + + if (lifetime == TimeSpan.Zero) + return false; - public bool TryHitCache(string? callingStationId, string userName, string clientName, TimeSpan lifetime) + if (string.IsNullOrWhiteSpace(callingStationId)) { - 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; - } + _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; + 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}"); - - if (authenticatedClient.Elapsed <= lifetime) - return true; - - _authenticatedClients.TryRemove(id, out _); - - return false; + return true; } - public void SetCache(string? callingStationId, string? userName, string clientName, TimeSpan lifetime) - { - ArgumentException.ThrowIfNullOrWhiteSpace(clientName); - - if (lifetime == TimeSpan.Zero || string.IsNullOrWhiteSpace(callingStationId)) - return; + return false; + } - var client = AuthenticatedClient.Create(callingStationId, clientName, userName); - var added = false; - if (!_authenticatedClients.ContainsKey(client.Id)) - added = _authenticatedClients.TryAdd(client.Id, client); + public void SetCache(string? callingStationId, string? userName, string clientName, TimeSpan lifetime) + { + ArgumentException.ThrowIfNullOrWhiteSpace(clientName); + + if (lifetime == TimeSpan.Zero || string.IsNullOrWhiteSpace(callingStationId)) + return; - if (added) + var id = AuthenticatedClient.ParseId(callingStationId, clientName, userName); + + if (!_memoryCache.TryGetValue(id, out _)) + { + var client = new AuthenticatedClient([callingStationId, clientName, userName]); + var cacheOptions = new MemoryCacheEntryOptions { - var expirationDate = DateTimeOffset.Now.Add(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); + 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 + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs index 5c6c0a9d..5e647612 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs @@ -1,4 +1,5 @@ using System.Text; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using DictionaryAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryAttribute; using DictionaryVendorAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryVendorAttribute; @@ -9,21 +10,23 @@ 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(string? filePath = null, string? appPath = null) + public RadiusDictionary(ApplicationVariables variables, string? filePath = null) { - _filePath = ResolveFilePath(filePath, appPath); + _variables = variables; + _filePath = ResolveFilePath(filePath); } - private string ResolveFilePath(string? filePath, string? appPath) + private string ResolveFilePath(string? filePath) { if (!string.IsNullOrEmpty(filePath) && Path.IsPathRooted(filePath)) return filePath; - var basePath = string.IsNullOrEmpty(appPath) + var basePath = string.IsNullOrEmpty(_variables.AppPath) ? AppDomain.CurrentDomain.BaseDirectory - : appPath; + : _variables.AppPath; var relativePath = string.IsNullOrEmpty(filePath) ? Path.Combine("content", "radius.dictionary") @@ -68,7 +71,7 @@ private void ProcessLine(string line) } } - private string[] SplitLine(string line) + private static string[] SplitLine(string line) { return line.Split(['\t', ' ', '\''], StringSplitOptions.RemoveEmptyEntries); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs index ed970938..08a8c2a4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -10,80 +10,4 @@ public InvalidConfigurationException(string 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. - /// - /// TODO fix and use or delete - // public static InvalidConfigurationException For(Expression> propertySelector, - // string formattedMessage, - // params object[] args) - // { - // ArgumentNullException.ThrowIfNull(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) - // { - // ArgumentNullException.ThrowIfNull(propertySelector); - // - // if (propertySelector.Body is not MemberExpression { Member: 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; - // return string.IsNullOrWhiteSpace(description) ? property.Name : 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 index 9ac1d583..d931eafe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -9,19 +9,20 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; public class ConfigurationLoader : IConfigurationLoader { private readonly IConfigurationParser _parser; - private readonly ILogger _logger; public ConfigurationLoader( - IConfigurationParser parser, - ILogger logger) + IConfigurationParser parser) { _parser = parser; - _logger = logger; + } + + public ServiceConfiguration Load() + { + return LoadAsync(CancellationToken.None).GetAwaiter().GetResult(); } public async Task LoadAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Loading configuration..."); var rootConfig = await LoadRootConfigurationAsync(cancellationToken); @@ -33,8 +34,6 @@ public async Task LoadAsync(CancellationToken cancellation ClientsConfigurations = clients }; - _logger.LogInformation("Configuration loaded: {ClientCount} clients", clients.Count); - return serviceConfig; } @@ -57,26 +56,24 @@ private async Task> LoadClientConfigurationsAsync( if (!Directory.Exists(clientsPath)) { - _logger.LogDebug("No clients directory found at {Path}", clientsPath); return clients; } var configFiles = Directory.GetFiles(clientsPath, "*.config"); + if (configFiles.Length == 0) + { + var assemblyLocation = Assembly.GetEntryAssembly()?.Location; + var configPath = $"{assemblyLocation}.config"; + if (!File.Exists(configPath)) + throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); + + return [await _parser.ParseClientConfigAsync(configPath, ct)]; + } foreach (var file in configFiles) { - try - { - var clientDto = await _parser.ParseClientConfigAsync(file, ct); - clients.Add(clientDto); - - _logger.LogDebug("Loaded client: {Name} from {File}", clientDto.Name, Path.GetFileName(file)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to load client configuration from {File}", file); - throw; - } + var clientDto = await _parser.ParseClientConfigAsync(file, ct); + clients.Add(clientDto); } return clients; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs index f0b899df..f1bb90bb 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs @@ -4,5 +4,6 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; public interface IConfigurationLoader { + ServiceConfiguration Load(); Task LoadAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs similarity index 78% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs index ec9b7e45..c733c831 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/ValueParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs @@ -1,20 +1,16 @@ using System.Globalization; using System.Net; -using Microsoft.Extensions.Logging; using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using NetTools; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; -public class ValueParser : IValueParser +public static class ValueParser { - private readonly ILogger _logger; - - public ValueParser(ILogger logger) => _logger = logger; - - public T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct + public static T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct { if (string.IsNullOrWhiteSpace(value)) { @@ -25,7 +21,7 @@ public T ParseEnum(string? value, T defaultValue = default, bool required = f : throw new InvalidConfigurationException($"Invalid value '{value}' for enum {typeof(T).Name}"); } - public bool ParseBool(string? value, bool defaultValue) + public static bool ParseBool(string? value, bool defaultValue) { if (string.IsNullOrWhiteSpace(value)) return defaultValue; @@ -33,7 +29,7 @@ public bool ParseBool(string? value, bool defaultValue) return bool.TryParse(value, out var result) ? result : defaultValue; } - public int ParseInt(string? value, int defaultValue) + public static int ParseInt(string? value, int defaultValue) { if (string.IsNullOrWhiteSpace(value)) return defaultValue; @@ -41,7 +37,7 @@ public int ParseInt(string? value, int defaultValue) return int.TryParse(value, out var result) ? result : defaultValue; } - public TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null) + public static TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null) { if (string.IsNullOrWhiteSpace(value)) return defaultValue ?? TimeSpan.Zero; @@ -52,7 +48,7 @@ public TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null) throw new InvalidConfigurationException($"Invalid time span format: '{value}'"); } - public TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) + public static TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) { if (string.IsNullOrWhiteSpace(value)) return defaultValue; @@ -73,13 +69,13 @@ public TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) { if (forced) { - _logger.LogWarning( + StartupLogger.Warning( "Timeout {Timeout}s is less than recommended minimum {Recommended}s", result.TotalSeconds, recommendedMin.TotalSeconds); } else { - _logger.LogWarning( + StartupLogger.Warning( "Timeout {Timeout}s is less than recommended minimum {Recommended}s. Use 'value!' to force", result.TotalSeconds, recommendedMin.TotalSeconds); result = recommendedMin; @@ -89,7 +85,7 @@ public TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) return result; } - public IPEndPoint? ParseEndpoint(string? value, bool required = false) + public static IPEndPoint? ParseEndpoint(string? value, bool required = false) { if (string.IsNullOrWhiteSpace(value)) { @@ -101,7 +97,7 @@ public TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) : throw new InvalidConfigurationException($"Invalid endpoint format: '{value}'"); } - public IPEndPoint[] ParseEndpoints(string? value, char separator, bool required = false) + public static IPEndPoint[] ParseEndpoints(string? value, char separator = ';', bool required = false) { if (string.IsNullOrWhiteSpace(value)) { @@ -113,25 +109,14 @@ public IPEndPoint[] ParseEndpoints(string? value, char separator, bool required : throw new InvalidConfigurationException($"Invalid endpoint format: '{endpoint}'")).ToArray(); } - - public Uri? ParseUri(string? value, bool required = false) - { - if (string.IsNullOrWhiteSpace(value)) - { - return required ? throw new InvalidConfigurationException("URI is required") : null; - } - - return Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri - : throw new InvalidConfigurationException($"Invalid URI format: '{value}'"); - } - public IPAddress? ParseIpAddress(string? value, bool required = false) + public static IPAddress? ParseIpAddress(string? value, bool required = false) { return IPAddress.TryParse(value, out var result) ? result : throw new InvalidConfigurationException($"Invalid IP address format: '{value}'"); } - public IReadOnlyList ParseUrls(string? value, bool required = false) + public static IReadOnlyList ParseUrls(string? value, bool required = false) { if (string.IsNullOrWhiteSpace(value)) { @@ -157,7 +142,7 @@ public IReadOnlyList ParseUrls(string? value, bool required = false) return urls; } - public IReadOnlyList ParseIpRanges(string? value) + public static IReadOnlyList ParseIpRanges(string? value) { if (string.IsNullOrWhiteSpace(value)) return []; @@ -181,7 +166,7 @@ public IReadOnlyList ParseIpRanges(string? value) return ranges; } - public IReadOnlyList ParseDistinguishedNames(string? value) + public static IReadOnlyList ParseDistinguishedNames(string? value) { if (string.IsNullOrWhiteSpace(value)) return []; @@ -208,7 +193,7 @@ public IReadOnlyList ParseDistinguishedNames(string? value) return names; } - public IReadOnlyList ParseStringList(string? value, char separator = ';') + public static IReadOnlyList ParseStringList(string? value, char separator = ';') { if (string.IsNullOrWhiteSpace(value)) return []; @@ -220,13 +205,13 @@ public IReadOnlyList ParseDistinguishedNames(string? value) } - public (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value) + public static (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value) { if (string.IsNullOrWhiteSpace(value)) return (PrivacyMode.None, []); var parts = value.Split(':', 2); - var mode = ParseEnum(parts[0], PrivacyMode.None); + var mode = ParseEnum(parts[0], PrivacyMode.None); if (parts.Length == 1) return (mode, []); @@ -239,7 +224,7 @@ public IReadOnlyList ParseDistinguishedNames(string? value) return (mode, fields); } - public (int min, int max) ParseDelaySettings(string value) + public static (int min, int max) ParseDelaySettings(string value) { if (string.IsNullOrWhiteSpace(value)) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs deleted file mode 100644 index f3f98e2d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser/IValueParser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Net; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; - -public interface IValueParser -{ - T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct; - bool ParseBool(string? value, bool defaultValue); - int ParseInt(string? value, int defaultValue); - TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null); - TimeSpan ParseTimeout(string? value, TimeSpan defaultValue); - IPEndPoint? ParseEndpoint(string? value, bool required = false); - public IPEndPoint[] ParseEndpoints(string? value, char separator = ';', bool required = false); - Uri? ParseUri(string? value, bool required = false); - IPAddress? ParseIpAddress(string? value, bool required = false); - IReadOnlyList ParseUrls(string? value, bool required = false); - IReadOnlyList ParseIpRanges(string? value); - IReadOnlyList ParseDistinguishedNames(string? value); - IReadOnlyList ParseStringList(string? value, char separator = ';'); - (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value); - (int min, int max) ParseDelaySettings(string value); -} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs index 9662b8cc..89d64da3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs @@ -1,46 +1,39 @@ using System.Net; using System.Xml.Linq; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; -using Multifactor.Radius.Adapter.v2.Shared; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; public class XmlConfigurationParser : IConfigurationParser { - private readonly IXmlReader _xmlReader; - private readonly IValueParser _valueParser; private readonly IRadiusDictionary _dictionary; public XmlConfigurationParser( - IXmlReader xmlReader, - IValueParser valueParser, IRadiusDictionary dictionary) { - _xmlReader = xmlReader; - _valueParser = valueParser; _dictionary = dictionary; } public async Task ParseRootConfigAsync(string filePath, CancellationToken ct) { - var xml = await _xmlReader.ReadAsync(filePath, ct); - var settings = _xmlReader.ExtractAppSettings(xml); + var xml = await XmlReader.ReadAsync(filePath, ct); + var settings = XmlReader.ExtractAppSettings(xml); - return new RootConfiguration() + return new RootConfiguration { - AdapterServerEndpoint = _valueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-server-endpoint"), required: true), - MultifactorApiUrls = _valueParser.ParseUrls(settings.GetValueOrDefault("multifactor-api-url"), required: true), - MultifactorApiTimeout = _valueParser.ParseTimeout(settings.GetValueOrDefault("multifactor-api-timeout"), - TimeSpan.FromSeconds(65)), + MultifactorApiUrls = ValueParser.ParseUrls(settings.GetValueOrDefault("multifactor-api-url"), required: true), MultifactorApiProxy = settings.GetValueOrDefault("multifactor-api-proxy"), + MultifactorApiTimeout = ValueParser.ParseTimeout(settings.GetValueOrDefault("multifactor-api-timeout"), + TimeSpan.FromSeconds(65)), + AdapterServerEndpoint = ValueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-server-endpoint"), required: true), LoggingFormat = settings.GetValueOrDefault("logging-format") ?? string.Empty, - SyslogUseTls = _valueParser.ParseBool("syslog-use-tls", false), + SyslogUseTls = ValueParser.ParseBool("syslog-use-tls", false), SyslogServer = settings.GetValueOrDefault("syslog-server") ?? string.Empty, SyslogFormat = settings.GetValueOrDefault("syslog-format") ?? string.Empty, SyslogFacility = settings.GetValueOrDefault("syslog-facility") ?? string.Empty, @@ -49,14 +42,15 @@ public async Task ParseRootConfigAsync(string filePath, Cance SyslogOutputTemplate = settings.GetValueOrDefault("syslog-output-template") ?? string.Empty, ConsoleLogOutputTemplate = settings.GetValueOrDefault("console-log-output-template") ?? string.Empty, FileLogOutputTemplate = settings.GetValueOrDefault("file-log-output-template") ?? string.Empty, - LogFileMaxSizeBytes = _valueParser.ParseInt("log-file-max-size-bytes", 1073741824), + LogFileMaxSizeBytes = ValueParser.ParseInt("log-file-max-size-bytes", 1073741824), + LoggingLevel = settings.GetValueOrDefault("logging-level"), }; } public async Task ParseClientConfigAsync(string filePath, CancellationToken ct) { - var xml = await _xmlReader.ReadAsync(filePath, ct); - var settings = _xmlReader.ExtractAppSettings(xml); + var xml = await XmlReader.ReadAsync(filePath, ct); + var settings = XmlReader.ExtractAppSettings(xml); var dto = new ClientConfiguration { @@ -65,41 +59,36 @@ public async Task ParseClientConfigAsync(string filePath, C ?? throw new InvalidConfigurationException("multifactor-nas-identifier is required"), MultifactorSharedSecret = settings.GetValueOrDefault("multifactor-shared-secret") ?? throw new InvalidConfigurationException("multifactor-shared-secret is required"), - SignUpGroups = _valueParser.ParseStringList(settings.GetValueOrDefault("sign-up-group")), - BypassSecondFactorWhenApiUnreachable = _valueParser.ParseBool("bypass-second-factor-when-api-unreachable", true), - FirstFactorAuthenticationSource = _valueParser.ParseEnum("first-factor-authentication-source", required: true), - AdapterClientEndpoint = _valueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-client-endpoint"), required: true), - RadiusClientIp = _valueParser.ParseIpAddress(settings.GetValueOrDefault("radius-client-ip")), + SignUpGroups = ValueParser.ParseStringList(settings.GetValueOrDefault("sign-up-group")), + BypassSecondFactorWhenApiUnreachable = ValueParser.ParseBool(settings.GetValueOrDefault("bypass-second-factor-when-api-unreachable"), true), + FirstFactorAuthenticationSource = ValueParser.ParseEnum(settings.GetValueOrDefault("first-factor-authentication-source"), required: true), + AdapterClientEndpoint = ValueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-client-endpoint"), required: true), + RadiusClientIp = ValueParser.ParseIpAddress(settings.GetValueOrDefault("radius-client-ip")), RadiusClientNasIdentifier = settings.GetValueOrDefault("radius-client-nas-identifier") ?? string.Empty, RadiusSharedSecret = settings.GetValueOrDefault("radius-shared-secret") ?? throw new InvalidConfigurationException("radius-shared-secret is required"), - NpsServerEndpoints = _valueParser.ParseEndpoints(settings.GetValueOrDefault("nps-server-endpoint"), required: true), - NpsServerTimeout = _valueParser.ParseTimeout("nps-server-timeout", TimeSpan.Parse("00:00:05")), - PreAuthenticationMethod = _valueParser.ParseEnum("pre-authentication-method", PreAuthMode.None), - AuthenticationCacheLifetime = _valueParser.ParseTimeSpan( + NpsServerEndpoints = ValueParser.ParseEndpoints(settings.GetValueOrDefault("nps-server-endpoint"), required: true), + NpsServerTimeout = ValueParser.ParseTimeout(settings.GetValueOrDefault("nps-server-timeout"), TimeSpan.Parse("00:00:05")), + Privacy = ValueParser.ParsePrivacyModeWithFields(settings.GetValueOrDefault("privacy-mode")), + PreAuthenticationMethod = ValueParser.ParseEnum(settings.GetValueOrDefault("pre-authentication-method"), PreAuthMode.None), + AuthenticationCacheLifetime = ValueParser.ParseTimeSpan( settings.GetValueOrDefault("authentication-cache-lifetime")), CallingStationIdAttribute = settings.GetValueOrDefault("calling-station-id-attribute"), - IpWhiteList = _valueParser.ParseIpRanges( + IpWhiteList = ValueParser.ParseIpRanges( settings.GetValueOrDefault("ip-white-list")), - LoggingLevel = settings.GetValueOrDefault("logging-level"), - InvalidCredentialDelay = _valueParser.ParseDelaySettings(settings.GetValueOrDefault("invalid-credential-delay")), + InvalidCredentialDelay = ValueParser.ParseDelaySettings(settings.GetValueOrDefault("invalid-credential-delay")), + ReplyAttributes = ParseReplyAttributes(xml) }; - - var (mode, fields) = _valueParser.ParsePrivacyModeWithFields(settings.GetValueOrDefault("privacy-mode")); - dto.PrivacyMode = mode; - dto.PrivacyFields = fields; dto.LdapServers = ParseLdapServers(xml, dto.Name); - dto.ReplyAttributes = ParseReplyAttributes(xml); - return dto; } private List ParseLdapServers(XDocument xml, string configName) { var servers = new List(); - var ldapElements = xml.Root?.Element("ldapServers")?.Elements("ldapServer"); + var ldapElements = XmlReader.GetLdapServerElements(xml); if (ldapElements == null) return servers; @@ -109,22 +98,22 @@ private List ParseLdapServers(XDocument xml, string con ConnectionString = element.Attribute("connection-string")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), Username = element.Attribute("username")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), Password = element.Attribute("password")?.Value ?? throw new InvalidConfigurationException("LDAP password is required"), - BindTimeoutSeconds = _valueParser.ParseInt(element.Attribute("bind-timeout-in-seconds")?.Value, 30), - AccessGroups = _valueParser.ParseDistinguishedNames(element.Attribute("access-groups")?.Value), - SecondFaGroups = _valueParser.ParseDistinguishedNames(element.Attribute("second-fa-groups")?.Value), - SecondFaBypassGroups = _valueParser.ParseDistinguishedNames(element.Attribute("second-fa-bypass-groups")?.Value), - LoadNestedGroups = _valueParser.ParseBool(element.Attribute("load-nested-groups")?.Value, true), - NestedGroupsBaseDns = _valueParser.ParseDistinguishedNames(element.Attribute("nested-groups-base-dn")?.Value), - AuthenticationCacheGroups = _valueParser.ParseDistinguishedNames(element.Attribute("authentication-cache-groups")?.Value), - PhoneAttributes = _valueParser.ParseStringList(element.Attribute("phone-attributes")?.Value), + BindTimeoutSeconds = ValueParser.ParseInt(element.Attribute("bind-timeout-in-seconds")?.Value, 30), + AccessGroups = ValueParser.ParseDistinguishedNames(element.Attribute("access-groups")?.Value), + SecondFaGroups = ValueParser.ParseDistinguishedNames(element.Attribute("second-fa-groups")?.Value), + SecondFaBypassGroups = ValueParser.ParseDistinguishedNames(element.Attribute("second-fa-bypass-groups")?.Value), + LoadNestedGroups = ValueParser.ParseBool(element.Attribute("load-nested-groups")?.Value, true), + NestedGroupsBaseDns = ValueParser.ParseDistinguishedNames(element.Attribute("nested-groups-base-dn")?.Value), + AuthenticationCacheGroups = ValueParser.ParseDistinguishedNames(element.Attribute("authentication-cache-groups")?.Value), + PhoneAttributes = ValueParser.ParseStringList(element.Attribute("phone-attributes")?.Value), IdentityAttribute = element.Attribute("identity-attribute")?.Value ?? "sAMAccountName", - RequiresUpn = _valueParser.ParseBool(element.Attribute("requires-upn")?.Value, false), - TrustedDomainsEnabled = _valueParser.ParseBool(element.Attribute("enable-trusted-domains")?.Value, false), - AlternativeSuffixesEnabled = _valueParser.ParseBool(element.Attribute("enable-alternative-suffixes")?.Value, false), - IncludedDomains = _valueParser.ParseStringList(element.Attribute("included-domains")?.Value), - ExcludedDomains = _valueParser.ParseStringList(element.Attribute("excluded-domains")?.Value), - IncludedSuffixes = _valueParser.ParseStringList(element.Attribute("included-suffixes")?.Value), - ExcludedSuffixes = _valueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value) + RequiresUpn = ValueParser.ParseBool(element.Attribute("requires-upn")?.Value, false), + TrustedDomainsEnabled = ValueParser.ParseBool(element.Attribute("enable-trusted-domains")?.Value, false), + AlternativeSuffixesEnabled = ValueParser.ParseBool(element.Attribute("enable-alternative-suffixes")?.Value, false), + IncludedDomains = ValueParser.ParseStringList(element.Attribute("included-domains")?.Value), + ExcludedDomains = ValueParser.ParseStringList(element.Attribute("excluded-domains")?.Value), + IncludedSuffixes = ValueParser.ParseStringList(element.Attribute("included-suffixes")?.Value), + ExcludedSuffixes = ValueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value) })); return servers; @@ -133,7 +122,7 @@ private List ParseLdapServers(XDocument xml, string con private IReadOnlyDictionary ParseReplyAttributes( XDocument xml) { - var elements = _xmlReader.GetRadiusReplyElements(xml); + var elements = XmlReader.GetRadiusReplyElements(xml); if (!elements.Any()) return new Dictionary(); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs deleted file mode 100644 index 820ff3f9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/IXmlReader.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Xml.Linq; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; - -public interface IXmlReader -{ - Task ReadAsync(string filePath, CancellationToken cancellationToken); - IReadOnlyDictionary ExtractAppSettings(XDocument xml); - IReadOnlyList GetLdapServerElements(XDocument xml); - IReadOnlyList GetRadiusReplyElements(XDocument xml); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs index 85478d17..21d7533b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs @@ -5,16 +5,12 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; -public class XmlReader : IXmlReader +public static class XmlReader { - private readonly ILogger _logger; - public XmlReader(ILogger logger) => _logger = logger; - - public async Task ReadAsync(string filePath, CancellationToken cancellationToken) + public static async Task ReadAsync(string filePath, CancellationToken cancellationToken) { try { - _logger.LogDebug("Reading XML configuration from {FilePath}", filePath); await using var stream = File.OpenRead(filePath); var settings = new XmlReaderSettings @@ -31,12 +27,11 @@ public async Task ReadAsync(string filePath, CancellationToken cancel } catch (Exception ex) when (ex is not OperationCanceledException) { - _logger.LogError(ex, "Failed to read XML file {FilePath}", filePath); throw new InvalidConfigurationException($"Failed to read configuration file: {filePath}", ex); } } - public IReadOnlyDictionary ExtractAppSettings(XDocument xml) + public static IReadOnlyDictionary ExtractAppSettings(XDocument xml) { var appSettings = xml.Root?.Element("appSettings"); if (appSettings == null) @@ -50,13 +45,13 @@ public IReadOnlyDictionary ExtractAppSettings(XDocument xml) StringComparer.OrdinalIgnoreCase); } - public IReadOnlyList GetLdapServerElements(XDocument xml) + public static IReadOnlyList GetLdapServerElements(XDocument xml) { return xml.Root?.Element("ldapServers")?.Elements("ldapServer").ToList() ?? []; } - public IReadOnlyList GetRadiusReplyElements(XDocument xml) + public static IReadOnlyList GetRadiusReplyElements(XDocument xml) { var attributes = xml.Root?.Element("RadiusReply")?.Element("Attributes"); return attributes?.Elements("add").ToList() ?? []; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs index e478a57a..ef447901 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -1,5 +1,4 @@ using System.Security.Authentication; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,25 +11,29 @@ using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; using Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Ports; 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.AccessChallenge; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; -using Multifactor.Radius.Adapter.v2.Application.Ports; 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.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser.ValueParser; -using Multifactor.Radius.Adapter.v2.Infrastructure.Http; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; +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.Shared; +using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; using Polly; using Serilog; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -48,19 +51,17 @@ public static void AddConfiguration(this IServiceCollection services) dict.Read(); return dict; }); - - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => { var manager = provider.GetRequiredService(); - return manager.LoadAsync(CancellationToken.None).GetAwaiter().GetResult(); + return manager.Load(); }); } - public static IServiceCollection AddRadiusUdpClient(this IServiceCollection services) + public static void AddRadiusUdpClient(this IServiceCollection services) { services.AddSingleton(serviceProvider => { @@ -72,29 +73,45 @@ public static IServiceCollection AddRadiusUdpClient(this IServiceCollection serv return new CustomUdpClient(endpoint, logger, options); }); - return services; + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); } public static void AddMultifactorApi(this IServiceCollection services) { + services.AddTransient(); services.AddSingleton(); services.AddHttpClient("multifactor-api") - .AddPolicyHandler((serviceProvider, request) => + .ConfigureHttpClient((serviceProvider, client) => + { + var config = serviceProvider.GetRequiredService(); + if (config.RootConfiguration.MultifactorApiUrls?.Any() == true) + { + var primaryUrl = config.RootConfiguration.MultifactorApiUrls[0]; + client.BaseAddress = primaryUrl; + } + }) + .AddPolicyHandler((serviceProvider, request) => Policy .Handle() - .OrResult(response => !response.IsSuccessStatusCode) + .OrResult(response => !response.IsSuccessStatusCode && (int)response.StatusCode >= 500) .FallbackAsync( fallbackAction: async (outcome, context, cancellationToken) => { + StartupLogger.Error("start"); var urlSelector = serviceProvider.GetRequiredService(); var fallbackUrl = await urlSelector.GetNextEndpointAsync(); var fallbackRequest = request.CloneHttpRequestMessage(); - fallbackRequest.RequestUri = new Uri(new Uri(fallbackUrl), request.RequestUri!.PathAndQuery); + fallbackRequest.RequestUri = new Uri(fallbackUrl, request.RequestUri!.PathAndQuery); + StartupLogger.Error(fallbackRequest.RequestUri.ToString()); var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient("FallbackClient"); + var httpClient = httpClientFactory.CreateClient("multifactor-api"); return await httpClient.SendAsync(fallbackRequest, cancellationToken); }, @@ -144,7 +161,8 @@ public static void AddAdapterLogging(this IServiceCollection services) public static void AddLdap(this IServiceCollection services) { - services.AddSingleton(); + services.AddSingleton(LdapConnectionFactory.Create()); + services.AddSingleton((prov) => new CustomLdapConnectionFactory()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -155,6 +173,8 @@ public static void AddInfraServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddSingleton(); services.AddTransient(); @@ -163,7 +183,10 @@ public static void AddInfraServices(this IServiceCollection services) public static void AddPipelines(this IServiceCollection services) { + + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - } + } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index 80e501fb..8512eaab 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -1,10 +1,13 @@ +using Elastic.CommonSchema.Serilog; 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; @@ -35,16 +38,15 @@ public static ILogger CreateLogger(RootConfiguration rootConfiguration) rootConfiguration.SyslogAppName, rootConfiguration.SyslogUseTls ); - //TODO rework loglevel - // var level = rootConfiguration.LoggingLevel; - // if (string.IsNullOrWhiteSpace(level)) - // { - // throw InvalidConfigurationException.For(x => x.LoggingLevel, - // "'{prop}' element not found. Config name: '{0}'", - // rootConfiguration.ConfigurationName); - // } - - // SetLogLevel(levelSwitch, level); + var level = rootConfiguration.LoggingLevel; + if (string.IsNullOrWhiteSpace(level)) + { + // throw new InvalidConfigurationException( + // "'{prop}' element not found. Config name: '{0}'", + // rootConfiguration.ConfigurationName); + } + + SetLogLevel(levelSwitch, level); var logger = loggerConfiguration.CreateLogger(); return logger; @@ -82,8 +84,12 @@ private static void ConfigureLogging( if (!string.IsNullOrWhiteSpace(consoleTemplate)) loggerConfiguration.WriteTo.Console(outputTemplate: consoleTemplate); else - loggerConfiguration.WriteTo.Console(); - + // loggerConfiguration.WriteTo.Console();TODO remove + loggerConfiguration.WriteTo.Console( + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}", + theme: AnsiConsoleTheme.Code, + restrictedToMinimumLevel: LogEventLevel.Information); + if (!string.IsNullOrWhiteSpace(fileTemplate)) { loggerConfiguration.WriteTo.File( @@ -202,7 +208,7 @@ private static void SetLogLevel(LoggingLevelSwitch levelSwitch, string level) { SerilogJsonFormatterTypes.Json or SerilogJsonFormatterTypes.JsonUtc => new RenderedCompactJsonFormatter(), SerilogJsonFormatterTypes.JsonTz => new CustomCompactJsonFormatter("yyyy-MM-dd HH:mm:ss.fff zzz"), - // SerilogJsonFormatterTypes.ElasticCommonSchema => new EcsTextFormatter(), + SerilogJsonFormatterTypes.ElasticCommonSchema => new EcsTextFormatter(), _ => null, }; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs index 1c385f11..aef234b4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs @@ -18,20 +18,20 @@ public static class StartupLogger { SelfLog.Enable(Console.WriteLine); - var baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); - var dir = Path.Combine(baseDir!, LogDirectory); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - var path = Path.Combine(dir, StartupLogFile); + // var baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); + // var dir = Path.Combine(baseDir!, LogDirectory); + // if (!Directory.Exists(dir)) + // { + // Directory.CreateDirectory(dir); + // } + + // var path = Path.Combine(dir, StartupLogFile); var loggerConfig = new LoggerConfiguration() - .WriteTo.File(path: path, - LogEventLevel.Verbose, - FileLogTemplate, - fileSizeLimitBytes: FileSizeLimitBytes, - rollOnFileSizeLimit: true) + // .WriteTo.File(path: path, + // LogEventLevel.Verbose, + // FileLogTemplate, + // fileSizeLimitBytes: FileSizeLimitBytes, + // rollOnFileSizeLimit: true) .WriteTo.Console(LogEventLevel.Verbose, ConsoleLogTemplate) .Enrich.FromLogContext(); @@ -46,6 +46,7 @@ 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); 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 index db7e764c..a55338a5 100644 --- 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 @@ -7,11 +7,12 @@ + - + diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs index 03dc7f91..5f46788b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs @@ -8,7 +8,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; public class RadiusPipeline : IRadiusPipeline { private readonly List _steps; - private readonly ILogger _logger; + // private readonly ILogger _logger; public RadiusPipeline(List steps) { @@ -18,7 +18,7 @@ public RadiusPipeline(List steps) public async Task ExecuteAsync(RadiusPipelineContext context) { - _logger.LogDebug("Starting pipeline execution with {StepCount} steps", _steps.Count); + // _logger.LogDebug("Starting pipeline execution with {StepCount} steps", _steps.Count); foreach (var step in _steps) { @@ -26,8 +26,8 @@ public async Task ExecuteAsync(RadiusPipelineContext context) if (context.IsTerminated) { - _logger.LogDebug("Pipeline terminated early at step {StepName}", - step.GetType().Name); + // _logger.LogDebug("Pipeline terminated early at step {StepName}", + // step.GetType().Name); break; } } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs index 35b32211..52872747 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs @@ -1,11 +1,10 @@ +using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Configuration; 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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; @@ -25,17 +24,19 @@ public RadiusPipelineFactory( public IRadiusPipeline CreatePipeline(ClientConfiguration clientConfig) { var steps = CreatePipelineSteps(clientConfig); + LogProviderCreated(clientConfig.Name, steps); return new RadiusPipeline(steps); } private List CreatePipelineSteps(ClientConfiguration clientConfig) { - var steps = new List(); - - steps.Add(CreateStep()); - steps.Add(CreateStep()); - steps.Add(CreateStep()); - + var steps = new List + { + CreateStep(), + CreateStep(), + CreateStep() + }; + if (clientConfig.LdapServers?.Count > 0) { steps.Add(CreateStep()); @@ -72,7 +73,20 @@ private IRadiusPipelineStep CreateStep() where TStep : IRadiusPipelineSte return _serviceProvider.GetRequiredService(); } - private bool ShouldLoadUserGroups(ClientConfiguration config) => config + 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(ClientConfiguration config) => config .ReplyAttributes .Values .SelectMany(x => x) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs index a8409359..90b20baf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs @@ -1,9 +1,7 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Configuration; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; @@ -24,29 +22,14 @@ public RadiusPipelineProvider( _serviceConfiguration = serviceConfiguration; } - public IRadiusPipeline GetPipeline(string clientName) + public IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) { + var clientName = clientConfiguration.Name; return _pipelineCache.GetOrAdd(clientName, name => { _logger.LogDebug("Creating new pipeline for client '{Client}'", name); - return _pipelineFactory.CreatePipeline(GetClientConfiguration(name)); + return _pipelineFactory.CreatePipeline(clientConfiguration); }); } - private ClientConfiguration GetClientConfiguration(string clientName) - { - return _serviceConfiguration.GetClientConfiguration(clientName); - } - - public void ClearCache() - { - _pipelineCache.Clear(); - _logger.LogInformation("Pipeline cache cleared"); - } - - public void RemoveFromCache(string clientName) - { - _pipelineCache.TryRemove(clientName, out _); - _logger.LogDebug("Removed pipeline for client '{Client}' from cache", clientName); - } } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs new file mode 100644 index 00000000..a20d68b5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs @@ -0,0 +1,8 @@ +using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; + +public interface IRadiusAttributeSerializer +{ + byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null); +} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs index 1e57d433..1013a200 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs @@ -2,16 +2,12 @@ using System.Text; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Security; +using Multifactor.Radius.Adapter.v2.Application.Features.Security; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; -public interface IRadiusAttributeSerializer -{ - byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null); -} public class RadiusAttributeSerializer : IRadiusAttributeSerializer { @@ -110,7 +106,7 @@ private byte[] ConvertValueToBytes(object value, string type) } } - private byte[] ConvertIntegerToBytes(object value) + private static byte[] ConvertIntegerToBytes(object value) { int intValue; @@ -143,7 +139,7 @@ private byte[] ConvertIntegerToBytes(object value) return bytes; } - private byte[] ConvertDateToBytes(object value) + private static byte[] ConvertDateToBytes(object value) { DateTime date; @@ -166,7 +162,7 @@ private byte[] ConvertDateToBytes(object value) return bytes; } - private byte[] CreateStandardHeader(byte typeCode, int contentLength) + private static byte[] CreateStandardHeader(byte typeCode, int contentLength) { var header = new byte[2]; header[0] = typeCode; @@ -174,7 +170,7 @@ private byte[] CreateStandardHeader(byte typeCode, int contentLength) return header; } - private byte[] CreateVendorSpecificHeader(DictionaryVendorAttribute vendorAttribute, int contentLength) + private static byte[] CreateVendorSpecificHeader(DictionaryVendorAttribute vendorAttribute, int contentLength) { // VSA format: Type(1)=26, Length(1), Vendor-Id(4), Vendor-Type(1), Vendor-Length(1), Content var header = new byte[8]; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs index e85633c9..10728cfa 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -1,7 +1,6 @@ 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.Application.Features.Radius.Ports; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; @@ -132,7 +131,6 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) break; } - // Copy authenticator to packet Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); return packetBytesArray; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs index ac021358..d8ea11df 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs @@ -10,12 +10,27 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; public sealed class RadiusClient : IRadiusClient { private readonly UdpClient _udpClient; - - // TODO ConcurrentDictionary -> MemoryCache - private readonly ConcurrentDictionary, TaskCompletionSource> _pendingRequests = new(); - + 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 @@ -24,37 +39,66 @@ 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); - } + // Запускаем периодическую очистку старых запросов + _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 responseTaskCs = new TaskCompletionSource(); + 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); - if (_pendingRequests.TryAdd(new Tuple(identifier, remoteEndpoint), responseTaskCs)) + try { - await _udpClient.SendAsync(requestPacket, requestPacket.Length, remoteEndpoint); - var completedTask = await Task.WhenAny(responseTaskCs.Task, Task.Delay(timeout)); - if (completedTask == responseTaskCs.Task) + if (_pendingRequests.TryAdd(key, pendingRequest)) { - return responseTaskCs.Task.Result.Buffer; + await _udpClient.SendAsync(requestPacket, remoteEndpoint); + + // Ожидаем завершения задачи (ответ или таймаут) + return await pendingRequest.TaskCompletionSource.Task; } - - _logger.LogDebug("Server {remoteEndpoint:l} did not respond within {timeout:l}", remoteEndpoint, timeout.ToString()); + 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; } - - _logger.LogWarning("Network error"); - return null; + finally + { + timeoutCancellation.Dispose(); + + _pendingRequests.TryRemove(key, out _); + } } /// @@ -62,31 +106,141 @@ public RadiusClient(IPEndPoint localEndpoint, ILogger logger) /// private async Task StartReceiveLoopAsync(CancellationToken cancellationToken) { + _logger.LogDebug("Starting receive loop"); + 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); + ProcessReceivedPacket(response); } catch (ObjectDisposedException) { // This is thrown when udpclient is disposed, can be safely ignored + break; } - catch (TaskCanceledException) + 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; } - await Task.Delay(TimeSpan.FromMilliseconds(5), cancellationToken); + 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() { - _cancellationTokenSource.Cancel(); - _udpClient?.Close(); + 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.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs index 88c70a0d..c5d9bf05 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/IRadiusCryptoProvider.cs @@ -8,6 +8,5 @@ public interface IRadiusCryptoProvider 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); - byte[] EncryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] password); byte[] 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 index ba56e668..b6b070c5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs @@ -1,7 +1,6 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; @@ -26,18 +25,13 @@ public byte[] CalculateResponseAuthenticator(SharedSecret secret, byte[] request public byte[] CalculateMessageAuthenticator(SharedSecret secret, byte[] packet, RadiusAuthenticator? requestAuth = null) { - using var hmac = new HMACMD5(secret.Bytes.ToArray()); - - if (requestAuth != null) - { - // For response packets, use request authenticator - var tempPacket = new byte[packet.Length]; - packet.CopyTo(tempPacket, 0); - requestAuth.Value.CopyTo(tempPacket, 4); - return hmac.ComputeHash(tempPacket); - } - - return hmac.ComputeHash(packet); + 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( @@ -55,13 +49,8 @@ public bool ValidateMessageAuthenticator( tempPacket[position + 2 + i] = 0; } var calculated = CalculateMessageAuthenticator(secret, tempPacket, requestAuth); - - return CryptographicOperations.FixedTimeEquals(calculated, messageAuth); - } - - public byte[] EncryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] password) - { - return RadiusPasswordProtector.Encrypt(secret, authenticator, password); + return calculated.SequenceEqual(messageAuth); + // return CryptographicOperations.FixedTimeEquals(calculated, messageAuth); } public byte[] DecryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword) @@ -69,7 +58,7 @@ public byte[] DecryptPassword(SharedSecret secret, RadiusAuthenticator authentic return RadiusPasswordProtector.Decrypt(secret, authenticator, encryptedPassword); } - private byte[] CalculateAuthenticator(SharedSecret secret, byte[] packet, byte[] requestAuth) + private static byte[] CalculateAuthenticator(SharedSecret secret, byte[] packet, byte[] requestAuth) { var buffer = new byte[packet.Length + secret.Bytes.Length]; packet.CopyTo(buffer, 0); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs index 1fa94f30..7b689d51 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusPasswordProtector.cs @@ -1,5 +1,4 @@ using System.Security.Cryptography; -using System.Text; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; @@ -41,18 +40,4 @@ public static byte[] Decrypt(SharedSecret secret, RadiusAuthenticator authentica { return Encrypt(secret, authenticator, encryptedPassword); // XOR is symmetric } - - public static string EncryptPasswordString(SharedSecret secret, RadiusAuthenticator authenticator, string password) - { - var passwordBytes = Encoding.UTF8.GetBytes(password); - var encryptedBytes = Encrypt(secret, authenticator, passwordBytes); - return Convert.ToBase64String(encryptedBytes); - } - - public static string DecryptPasswordString(SharedSecret secret, RadiusAuthenticator authenticator, string encryptedPassword) - { - var encryptedBytes = Convert.FromBase64String(encryptedPassword); - var decryptedBytes = Decrypt(secret, authenticator, encryptedBytes); - return Encoding.UTF8.GetString(decryptedBytes).TrimEnd('\0'); - } } \ 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 index c4186893..cf5a132e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -2,7 +2,6 @@ using System.Text; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; @@ -129,7 +128,7 @@ public RadiusAttributeParser( return new ParsedAttribute(attributeDefinition.Name, content, isMessageAuthenticator); } - private object? ParseContentBytes( + private object ParseContentBytes( byte[] contentBytes, string type, uint code, @@ -160,7 +159,7 @@ public RadiusAttributeParser( return ParseDate(contentBytes); case "ifid": - return ParseInterfaceId(contentBytes); + return contentBytes; default: _logger.LogWarning("Unknown attribute type: {Type}", type); @@ -168,7 +167,7 @@ public RadiusAttributeParser( } } - private string ParseString(byte[] bytes) + private static string ParseString(byte[] bytes) { // Try to decode as UTF-8, fall back to ASCII if invalid try @@ -181,40 +180,32 @@ private string ParseString(byte[] bytes) } } - private int ParseInteger(byte[] bytes) + private static int ParseInteger(byte[] bytes) { - if (bytes.Length == 4) - { - Array.Reverse(bytes); - return BitConverter.ToInt32(bytes, 0); - } - else if (bytes.Length == 2) - { - Array.Reverse(bytes); - return BitConverter.ToInt16(bytes, 0); - } - else if (bytes.Length == 1) + switch (bytes.Length) { - return bytes[0]; + case 4: + Array.Reverse(bytes); + return BitConverter.ToInt32(bytes, 0); + case 2: + Array.Reverse(bytes); + return BitConverter.ToInt16(bytes, 0); + case 1: + return bytes[0]; + default: + throw new InvalidOperationException($"Invalid integer length: {bytes.Length}"); } - - throw new InvalidOperationException($"Invalid integer length: {bytes.Length}"); } - private IPAddress ParseIpAddress(byte[] bytes) + private static IPAddress ParseIpAddress(byte[] bytes) { return new IPAddress(bytes); } - private DateTime ParseDate(byte[] bytes) + private static DateTime ParseDate(byte[] bytes) { Array.Reverse(bytes); uint seconds = BitConverter.ToUInt32(bytes, 0); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds); } - - private byte[] ParseInterfaceId(byte[] bytes) - { - return bytes; // Return as-is for Interface-Id - } } \ 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 index 04a1be85..e0323eaa 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs @@ -1,7 +1,6 @@ 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.Application.Features.Radius.Ports; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; @@ -54,7 +53,7 @@ private RadiusPacket ParseInternal( return packet; } - private void ValidatePacketLength(byte[] packetBytes) + private static void ValidatePacketLength(byte[] packetBytes) { if (packetBytes.Length < 20) { @@ -62,9 +61,9 @@ private void ValidatePacketLength(byte[] packetBytes) } } - private void ValidatePacketLengthField(byte[] packetBytes) + private static void ValidatePacketLengthField(byte[] packetBytes) { - var declaredLength = BitConverter.ToUInt16(new[] { packetBytes[3], packetBytes[2] }, 0); + var declaredLength = BitConverter.ToUInt16([packetBytes[3], packetBytes[2]], 0); if (declaredLength != packetBytes.Length) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs index 108022ae..c758e436 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -1,11 +1,10 @@ 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; -using Multifactor.Radius.Adapter.v2.Application.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Ports; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; @@ -16,7 +15,6 @@ public class AdapterResponseSender : IResponseSender 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"; @@ -45,21 +43,19 @@ public async Task SendResponse(SendAdapterResponseRequest request) return; } - // Проверка специальных случаев if (ShouldProxyResponse(request)) { await ProxyResponseAsync(request); return; } - // Построение и отправка обычного ответа var responsePacket = BuildResponsePacket(request); await SendResponsePacketAsync(responsePacket, request); LogResponseSent(responsePacket, request); } - private bool ShouldProxyResponse(SendAdapterResponseRequest request) + private static bool ShouldProxyResponse(SendAdapterResponseRequest request) { // EAP challenge if (request.ResponsePacket?.IsEapMessageChallenge == true) @@ -126,28 +122,24 @@ private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) private void ProcessAccessAcceptResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) { - // Копируем атрибуты из исходного ответа (если есть) if (request.ResponsePacket != null) { CopyAttributes(request.ResponsePacket, responsePacket); } - // Добавляем reply-атрибуты AddReplyAttributes(responsePacket, request); } private void ProcessAccessRejectResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) { - // Для Reject копируем атрибуты только если это тоже Reject if (request.ResponsePacket?.Code == PacketCode.AccessReject) { CopyAttributes(request.ResponsePacket, responsePacket); } } - private void ProcessAccessChallengeResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) + private static void ProcessAccessChallengeResponse(RadiusPacket responsePacket, SendAdapterResponseRequest request) { - // Добавляем State атрибут если есть if (!string.IsNullOrWhiteSpace(request.ResponseInformation.State)) { responsePacket.ReplaceAttribute(StateAttribute, request.ResponseInformation.State); @@ -161,25 +153,23 @@ private void AddCommonAttributes(RadiusPacket responsePacket, SendAdapterRespons { responsePacket.ReplaceAttribute(ReplyMessageAttribute, request.ResponseInformation.ReplyMessage); } - + // Proxy-State AddProxyStateAttribute(request.RequestPacket, responsePacket); - + // Message-Authenticator (placeholder если нет) AddMessageAuthenticatorIfMissing(responsePacket); } - private void CopyAttributes(RadiusPacket source, RadiusPacket target) + 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); @@ -187,11 +177,10 @@ private void CopyAttributes(RadiusPacket source, RadiusPacket target) } } - private void AddProxyStateAttribute(RadiusPacket source, RadiusPacket target) + 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(); @@ -203,7 +192,7 @@ private void AddProxyStateAttribute(RadiusPacket source, RadiusPacket target) } } - private void AddMessageAuthenticatorIfMissing(RadiusPacket packet) + private static void AddMessageAuthenticatorIfMissing(RadiusPacket packet) { if (!packet.Attributes.ContainsKey(MessageAuthenticatorAttribute)) { @@ -225,10 +214,8 @@ private void AddReplyAttributes(RadiusPacket target, SendAdapterResponseRequest foreach (var attribute in attributes) { - // Удаляем старый атрибут target.RemoveAttribute(attribute.Key); - // Добавляем все значения foreach (var attrValue in attribute.Value) { target.AddAttributeValue(attribute.Key, attrValue); @@ -236,7 +223,7 @@ private void AddReplyAttributes(RadiusPacket target, SendAdapterResponseRequest } } - private PacketCode DetermineResponseCode(AuthenticationStatus firstFactorStatus, AuthenticationStatus secondFactorStatus) + private static PacketCode DetermineResponseCode(AuthenticationStatus firstFactorStatus, AuthenticationStatus secondFactorStatus) { var successfulFirstFactor = firstFactorStatus is AuthenticationStatus.Accept diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs index b74477c2..06c7dad4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs @@ -16,15 +16,12 @@ public RadiusAttributeTypeConverter(IRadiusDictionary 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; } @@ -40,25 +37,22 @@ private object ConvertStringToType(string stringValue, string attributeType) "integer" => ConvertToInteger(stringValue), "string" or "tagged-string" => stringValue, "octets" => ConvertToOctets(stringValue), - _ => stringValue // Неподдерживаемый тип - возвращаем как строку + _ => stringValue }; } private object ConvertToIpAddress(string stringValue) { - // Пробуем парсить как обычный IP-адрес if (IPAddress.TryParse(stringValue, out var ipAddress)) return ipAddress; - // Пробуем парсить как Microsoft RADIUS Framed IP Address (целое число) if (int.TryParse(stringValue, out var intValue)) return ConvertMsRadiusFramedIpAddress(intValue); - // Не удалось конвертировать - возвращаем исходную строку return stringValue; } - private IPAddress ConvertMsRadiusFramedIpAddress(int intValue) + private static IPAddress ConvertMsRadiusFramedIpAddress(int intValue) { // Microsoft RADIUS специфика: // Числа выше 2147483647 представляются как отрицательные @@ -86,9 +80,8 @@ private IPAddress ConvertMsRadiusFramedIpAddress(int intValue) return new IPAddress(ipBytes); } - private object ConvertToDateTime(string stringValue) + private static object ConvertToDateTime(string stringValue) { - // Пробуем парсить как DateTime if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)) @@ -96,7 +89,6 @@ private object ConvertToDateTime(string stringValue) return dateTime; } - // Пробуем парсить как Unix timestamp if (long.TryParse(stringValue, out var unixTimestamp)) { return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime; @@ -105,7 +97,7 @@ private object ConvertToDateTime(string stringValue) return stringValue; } - private object ConvertToInteger(string stringValue) + private static object ConvertToInteger(string stringValue) { if (int.TryParse(stringValue, out var intValue)) return intValue; @@ -113,7 +105,7 @@ private object ConvertToInteger(string stringValue) return stringValue; } - private object ConvertToOctets(string stringValue) + private static object ConvertToOctets(string stringValue) { // Для octets можно конвертировать из hex или base64 try 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 index 09e12cd5..a82ce309 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs @@ -114,9 +114,4 @@ public bool TryGetNasIdentifier(byte[] packetBytes, out string nasIdentifier) return _nasIdentifierExtractor.TryExtract(packetBytes, out nasIdentifier); } -} - -public interface INasIdentifierExtractor -{ - bool TryExtract(byte[] packetBytes, out string 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 index 1a8fa803..b9a63a1b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -2,12 +2,10 @@ using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic.CompilerServices; -using Multifactor.Radius.Adapter.v2.Application.Configuration; 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; +using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; @@ -76,7 +74,7 @@ private List ProcessAttribute( _logger.LogDebug( "Added attribute '{Attribute}': {Value}", - attributeName, + attributeName, GetLoggableValue(convertedValue)); } @@ -115,7 +113,7 @@ private bool ShouldIncludeAttribute(RadiusReplyAttribute attributeValue, GetRepl return true; } - private bool MatchesUserNameCondition(IReadOnlyList conditions, string? userName) + private static bool MatchesUserNameCondition(IReadOnlyList conditions, string? userName) { if (string.IsNullOrWhiteSpace(userName)) return false; @@ -135,7 +133,7 @@ private bool MatchesUserNameCondition(IReadOnlyList conditions, string? return false; } - private bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet userGroups) + private static bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet userGroups) { if (userGroups == null || userGroups.Count == 0) return false; @@ -145,7 +143,7 @@ private bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet .Any(group => string.Equals(group, condition, StringComparison.OrdinalIgnoreCase))); } - private List GetAttributeValues(RadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + private static List GetAttributeValues(RadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) { if (attributeValue.IsMemberOf) { @@ -181,7 +179,7 @@ private void LogResult(IDictionary> result) result.Count); } - private string GetLoggableValue(object value) + private static string GetLoggableValue(object value) { if (value is IPAddress ip) return ip.ToString(); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs index 3cec2b56..fa9de0f3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Validators/RadiusPacketValidator.cs @@ -1,7 +1,6 @@ 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.Application.Features.Radius.Ports; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Validators; @@ -25,7 +24,7 @@ public void ValidateRawPacket(byte[] packetBytes) throw new InvalidOperationException($"Packet too large: {packetBytes.Length} bytes"); byte code = packetBytes[0]; - if (!Enum.IsDefined(typeof(PacketCode), code)) + if (!Enum.IsDefined(typeof(PacketCode), (int)code)) throw new InvalidOperationException($"Invalid packet code: {code}"); ushort declaredLength = BitConverter.ToUInt16([packetBytes[3], packetBytes[2]], 0); @@ -80,13 +79,11 @@ public void ValidatePacketForSerialization(RadiusPacket packet) if (packet.Authenticator.Value.Length != 16) throw new InvalidOperationException("Authenticator must be 16 bytes for serialization"); - // Check for required attributes based on packet type switch (packet.Code) { case PacketCode.AccessAccept: case PacketCode.AccessReject: case PacketCode.AccessChallenge: - // Response packets should have request authenticator if (packet.RequestAuthenticator == null) { _logger.LogWarning("Response packet missing request authenticator: Code={Code}", packet.Code); @@ -99,13 +96,11 @@ public void ValidatePacketForSerialization(RadiusPacket packet) private void ValidateAccessRequest(RadiusPacket packet) { - // Access-Request should have User-Name if (!packet.HasAttribute("User-Name")) { _logger.LogWarning("Access-Request missing User-Name attribute"); } - // Should have either User-Password or CHAP-Password/Challenge bool hasPassword = packet.HasAttribute("User-Password"); bool hasChapPassword = packet.HasAttribute("CHAP-Password"); bool hasChapChallenge = packet.HasAttribute("CHAP-Challenge"); @@ -116,9 +111,8 @@ private void ValidateAccessRequest(RadiusPacket packet) } } - private void ValidateAccountingRequest(RadiusPacket packet) + private static void ValidateAccountingRequest(RadiusPacket packet) { - // Accounting-Request should have Acct-Status-Type if (!packet.HasAttribute("Acct-Status-Type")) { throw new InvalidOperationException("Accounting-Request missing Acct-Status-Type"); @@ -127,7 +121,6 @@ private void ValidateAccountingRequest(RadiusPacket packet) private void ValidateCoaRequest(RadiusPacket packet) { - // CoA/Disconnect should have specific attributes if (!packet.HasAttribute("User-Name") && !packet.HasAttribute("Acct-Session-Id")) { _logger.LogWarning("CoA/Disconnect request missing User-Name or Acct-Session-Id"); diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs new file mode 100644 index 00000000..7999d6b2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs @@ -0,0 +1,33 @@ +namespace Multifactor.Radius.Adapter.v2.Shared.Attributes; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class ConfigParameterAttribute : Attribute +{ + public string XmlName { get; } + public string EnvName { get; } + public object? DefaultValue { get; } + public bool Required { get; } + + public ConfigParameterAttribute(string xmlName, object? defaultValue = null) + { + XmlName = xmlName; + EnvName = ConvertToEnvName(xmlName); + DefaultValue = defaultValue; + } + + private static string ConvertToEnvName(string xmlName) + { + return xmlName.Replace('-', '_').ToUpperInvariant(); + } +} + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class ComplexConfigParameterAttribute : Attribute +{ + public string XmlElementName { get; } + + public ComplexConfigParameterAttribute(string xmlElementName) + { + XmlElementName = xmlElementName; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs similarity index 74% rename from src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs rename to src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs index 9cd0354b..d122eedc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Shared/BytesExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/BytesExtensions.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Shared; +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; public static class BytesExtensions { diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs similarity index 96% rename from src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs rename to src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs index 4e62f88c..419e8e11 100644 --- a/src/Multifactor.Radius.Adapter.v2.Shared/HttpRequestMessageExtension.cs +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/HttpRequestMessageExtension.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Multifactor.Radius.Adapter.v2.Shared; +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; public static class HttpRequestMessageExtension { diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs similarity index 96% rename from src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs rename to src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs index c46e088d..e176c732 100644 --- a/src/Multifactor.Radius.Adapter.v2.Shared/StringExtension.cs +++ b/src/Multifactor.Radius.Adapter.v2.Shared/Extensions/StringExtension.cs @@ -1,7 +1,6 @@ -using System.Net; using System.Text; -namespace Multifactor.Radius.Adapter.v2.Shared; +namespace Multifactor.Radius.Adapter.v2.Shared.Extensions; public static class StringExtension { From a42e35b5b0722b8ba2e6aee64583a809d82c39d7 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 21 Jan 2026 22:23:50 +0300 Subject: [PATCH 03/11] Second iteration --- src/Multifactor.Radius.Adapter.v2/App.config | 21 ++++++++++++-- src/Multifactor.Radius.Adapter.v2/Dockerfile | 8 +++-- .../Multifactor.Radius.Adapter.v2.csproj | 2 +- src/Multifactor.Radius.Adapter.v2/Program.cs | 29 +++++++++++-------- .../Server/AdapterServer.cs | 2 -- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/Multifactor.Radius.Adapter.v2/App.config b/src/Multifactor.Radius.Adapter.v2/App.config index 949abf36..157a1792 100644 --- a/src/Multifactor.Radius.Adapter.v2/App.config +++ b/src/Multifactor.Radius.Adapter.v2/App.config @@ -7,16 +7,31 @@ - + - + - + + + + + + - + + + + + + + + + + diff --git a/src/Multifactor.Radius.Adapter.v2/Dockerfile b/src/Multifactor.Radius.Adapter.v2/Dockerfile index b1b93e2c..445130f1 100644 --- a/src/Multifactor.Radius.Adapter.v2/Dockerfile +++ b/src/Multifactor.Radius.Adapter.v2/Dockerfile @@ -1,6 +1,8 @@ -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 @@ -18,4 +20,4 @@ RUN dotnet publish "RediusV2.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p: FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "RediusV2.dll"] +ENTRYPOINT ["dotnet", "RediusV2.dll"] \ 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 082580e8..ab96dce5 100644 --- a/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj +++ b/src/Multifactor.Radius.Adapter.v2/Multifactor.Radius.Adapter.v2.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/Multifactor.Radius.Adapter.v2/Program.cs b/src/Multifactor.Radius.Adapter.v2/Program.cs index 13170578..fffa2666 100644 --- a/src/Multifactor.Radius.Adapter.v2/Program.cs +++ b/src/Multifactor.Radius.Adapter.v2/Program.cs @@ -4,6 +4,7 @@ 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.Configurations.Exceptions; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; using Multifactor.Radius.Adapter.v2.Server; @@ -14,27 +15,26 @@ var builder = Host.CreateApplicationBuilder(args); builder.Services.AddWindowsService(options => options.ServiceName = "Multifactor RADIUS"); builder.Services.AddMemoryCache(); - builder.Services.AddAdapterLogging(); builder.Services.AddApplicationVariables(); - + builder.Services.AddConfiguration(); - + builder.Services.AddAdapterLogging(); + builder.Services.AddLdap(); - - builder.Services.AddFirstFactor(); + builder.Services.AddChallenge(); + builder.Services.AddFirstFactor(); builder.Services.AddPipelineSteps(); builder.Services.AddPipelines(); - + builder.Services.AddTransient(); - + builder.Services.AddInfraServices(); builder.Services.AddAppServices(); - builder.Services.AddChallenge(); - + builder.Services.AddRadiusUdpClient(); builder.Services.AddMultifactorApi(); - + builder.Services.AddSingleton(); builder.Services.AddHostedService(); host = builder.Build(); @@ -42,8 +42,13 @@ } catch (Exception ex) { - var errorMessage = FlattenException(ex); - StartupLogger.Error(ex, "Unable to start: {Message:l}", errorMessage); + if(ex is InvalidConfigurationException) + StartupLogger.Error(null, "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 e5dfbe2e..12ff336d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -1,10 +1,8 @@ using System.Collections.Concurrent; using System.Net.Sockets; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Configuration; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; -using Multifactor.Radius.Adapter.v2.Application.Ports; namespace Multifactor.Radius.Adapter.v2.Server; From b487323bcc554b1e58960e44303e7d47d919ae71 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 28 Jan 2026 13:18:26 +0300 Subject: [PATCH 04/11] End of refactoring --- .../Cache/ICacheService.cs | 2 +- .../Models/ClientConfiguration.cs | 2 +- .../Configuration/Models/Enum/PreAuthMode.cs | 1 + .../Configuration/Models/Enum/PrivacyMode.cs | 1 + .../Models/LdapServerConfiguration.cs | 6 +- .../Configuration/Models/RootConfiguration.cs | 1 - .../Models/ServiceConfiguration.cs | 4 +- .../Extensions/ApplicationExtensions.cs | 8 + .../Ldap/Models/ChangeUserPasswordRequest.cs | 5 +- .../Features/Ldap/Models/FindUserRequest.cs | 5 +- ...ectionRequest.cs => LdapConnectionData.cs} | 2 +- .../Features/Ldap/Models/LoadSchemaRequest.cs | 22 --- .../Ldap/Models/LoadUserGroupRequest.cs | 5 +- .../Features/Ldap/Models/MembershipRequest.cs | 17 +- .../Features/Ldap/{ => Ports}/ILdapAdapter.cs | 6 +- .../MultifactorApiUnreachableException.cs | 2 +- .../Multifactor/Models/AccessRequestQuery.cs | 1 - .../Multifactor/MultifactorApiService.cs | 59 ++++-- .../Multifactor/RequestDataExtractor.cs | 38 +--- .../ChallengeProcessorProvider.cs | 1 + .../ChangePasswordChallengeProcessor.cs | 27 +-- .../AccessChallenge/IChallengeProcessor.cs | 1 + .../IChallengeProcessorProvider.cs | 1 + .../Models/{ => Enums}/ChallengeStatus.cs | 2 +- .../Models/{ => Enums}/ChallengeType.cs | 2 +- .../SecondFactorChallengeProcessor.cs | 2 + .../BindNameFormat/MultiDirectoryFormatter.cs | 3 +- .../BindNameFormat/OpenLdapFormatter.cs | 3 +- .../BindNameFormat/SambaFormatter.cs | 3 +- .../FirstFactor/LdapFirstFactorProcessor.cs | 5 +- .../{ => Interfaces}/IPipelineProvider.cs | 2 +- .../{ => Interfaces}/IRadiusPipeline.cs | 2 +- .../IRadiusPipelineFactory.cs | 2 +- .../Features}/Pipeline/RadiusPipeline.cs | 10 +- .../Pipeline/RadiusPipelineFactory.cs | 4 +- .../Pipeline/RadiusPipelineProvider.cs | 9 +- .../Pipeline/Steps/AccessChallengeStep.cs | 1 + .../Steps/AccessGroupsCheckingStep.cs | 1 + .../Pipeline/Steps/FirstFactorStep.cs | 2 +- .../Pipeline/Steps/LdapSchemaLoadingStep.cs | 9 +- .../Pipeline/Steps/PreAuthCheckStep.cs | 27 ++- .../Pipeline/Steps/ProfileLoadingStep.cs | 14 +- .../Pipeline/Steps/SecondFactorStep.cs | 3 +- .../Steps/StatusServerFilteringStep.cs | 2 +- .../Pipeline/Steps/UserGroupLoadingStep.cs | 27 ++- .../Pipeline/Steps/UserNameValidationStep.cs | 14 +- .../Radius/Models/IResponseInformation.cs | 8 - .../Features/Radius/Ports/IUdpClient.cs | 3 +- .../Radius/Services/RadiusPacketProcessor.cs | 3 + .../Features/Security/ProtectionService.cs | 9 +- .../Security/RadiusPasswordProtector.cs | 4 +- .../Ldap/CustomLdapConnectionFactory.cs | 5 - .../Adapters/Ldap/LdapAdapter.cs | 57 ++---- .../Multifactor/Models/AccessRequestDto.cs | 2 +- .../Adapters/Multifactor/MultifactorApi.cs | 180 ++++++++++++++--- .../PacketHandler/RadiusUdpAdapter.cs | 1 + .../Adapters/Udp/CustomUdpClient.cs | 49 +---- .../AuthenticatedClient.cs | 8 - .../Loader/ConfigurationLoader.cs | 6 +- .../Parser/XmlConfigurationParser.cs | 104 ++++++---- .../Reader/EnvironmentReader.cs | 14 ++ .../Configurations/Reader/XmlReader.cs | 1 - .../Extensions/InfrastructureExtensions.cs | 18 +- .../Logging/SerilogLoggerFactory.cs | 33 ++-- .../Radius/Builders/RadiusPacketBuilder.cs | 185 ++++++++++++------ .../Radius/Client/RadiusClient.cs | 4 +- .../Radius/Crypto/RadiusCryptoProvider.cs | 28 ++- .../Radius/Parsers/IRadiusAttributeParser.cs | 3 +- .../Radius/Parsers/RadiusAttributeParser.cs | 120 ++++-------- .../Radius/Parsers/RadiusPacketParser.cs | 79 ++++---- .../Radius/Sender/AdapterResponseSender.cs | 54 ++--- .../Services/RadiusReplyAttributeService.cs | 5 +- src/Multifactor.Radius.Adapter.v2/App.config | 8 + src/Multifactor.Radius.Adapter.v2/Dockerfile | 29 ++- .../Server/AdapterServer.cs | 6 +- .../Server/ServerHost.cs | 3 +- 76 files changed, 761 insertions(+), 634 deletions(-) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/{CheckConnectionRequest.cs => LdapConnectionData.cs} (87%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs rename src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/{ => Ports}/ILdapAdapter.cs (78%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/{ => Enums}/ChallengeStatus.cs (79%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/{ => Enums}/ChallengeType.cs (80%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/{ => Interfaces}/IPipelineProvider.cs (93%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/{ => Interfaces}/IRadiusPipeline.cs (92%) rename src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/{ => Interfaces}/IRadiusPipelineFactory.cs (93%) rename src/{Multifactor.Radius.Adapter.v2.Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/RadiusPipeline.cs (60%) rename src/{Multifactor.Radius.Adapter.v2.Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/RadiusPipelineFactory.cs (97%) rename src/{Multifactor.Radius.Adapter.v2.Infrastructure => Multifactor.Radius.Adapter.v2.Application/Features}/Pipeline/RadiusPipelineProvider.cs (78%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs index bdf5b632..e9a18c1e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs @@ -2,7 +2,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface ICacheService { - //TODO check how use. + //TODO check where use. void Set(string key, T value, DateTimeOffset expirationDate); bool TryGetValue(string key, out T? value); void Remove(string key); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs index 3e0624bd..2d3136b6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs @@ -43,7 +43,7 @@ public class ClientConfiguration [ConfigParameter("invalid-credential-delay")] public (int min, int max) InvalidCredentialDelay { get; set; } [ConfigParameter("calling-station-id-attribute")] - public string CallingStationIdAttribute { get; set; } + public string CallingStationIdAttribute { get; set; } //TODO not used [ConfigParameter("ip-white-list")] public IReadOnlyList IpWhiteList { get; set; } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs index 450cf373..1dc28391 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PreAuthMode.cs @@ -1,5 +1,6 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum { + [Flags] public enum PreAuthMode { /// diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs index f6ee35b3..ae027614 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/Enum/PrivacyMode.cs @@ -7,6 +7,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; /// /// User information disclosure mode /// +[Flags] public enum PrivacyMode { /// diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs index 41f9d286..63fe2115 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs @@ -36,11 +36,13 @@ public class LdapServerConfiguration [ConfigParameter("enable-alternative-suffixes")] public bool AlternativeSuffixesEnabled { get; init; } [ConfigParameter("included-domains")] - public IReadOnlyList IncludedDomains { get; init; } + public IReadOnlyList IncludedDomains { get; init; }//TODO not used [ConfigParameter("excluded-domains")] - public IReadOnlyList ExcludedDomains { get; init; } + public IReadOnlyList ExcludedDomains { get; init; }//TODO not used [ConfigParameter("included-suffixes")] public IReadOnlyList IncludedSuffixes { get; init; } [ConfigParameter("excluded-suffixes")] public IReadOnlyList ExcludedSuffixes { get; init; } + [ConfigParameter("bypass-second-factor-when-api-unreachable-groups")] + public IReadOnlyList BypassSecondFactorWhenApiUnreachableGroups { get; init; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs index 4f5f81e4..90e5584e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs @@ -15,7 +15,6 @@ public class RootConfiguration public IPEndPoint? AdapterServerEndpoint { get; set; } [ConfigParameter("logging-level")] public string LoggingLevel { get; set; } - [ConfigParameter("logging-format")] public string LoggingFormat { get; set; } [ConfigParameter("syslog-use-tls")] diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs index 1915a8eb..ef5a14df 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -6,6 +6,6 @@ public class ServiceConfiguration { public required RootConfiguration RootConfiguration { get; set; } public required IReadOnlyList ClientsConfigurations { get; set; } - public ClientConfiguration GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientNasIdentifier == nasIdentifier); - public ClientConfiguration GetClientConfiguration(IPAddress ip) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); + public ClientConfiguration? GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientNasIdentifier == nasIdentifier); + public ClientConfiguration? GetClientConfiguration(IPAddress ip) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs index a8d2b47e..7cd8d370 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Extensions/ApplicationExtensions.cs @@ -2,9 +2,11 @@ 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; @@ -48,6 +50,12 @@ public static void AddChallenge(this IServiceCollection services) services.AddSingleton(); } + public static void AddPipelines(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + public static void AddPipelineSteps(this IServiceCollection services) { services.AddTransient(); 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 index 7b7e31a7..396d997c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/ChangeUserPasswordRequest.cs @@ -5,10 +5,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public class ChangeUserPasswordRequest { - public string ConnectionString { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public int BindTimeoutInSeconds { get; set; } + public LdapConnectionData ConnectionData { get; set; } public ILdapSchema LdapSchema { get; set; } public DistinguishedName DistinguishedName { get; set; } public string NewPassword { get; set; } 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 index d48a009d..c4c59efb 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/FindUserRequest.cs @@ -7,10 +7,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public class FindUserRequest { - public string ConnectionString { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public int BindTimeoutInSeconds { get; set; } + public LdapConnectionData ConnectionData { get; set; } public UserIdentity UserIdentity { get; set; } public DistinguishedName SearchBase { get; set; } public ILdapSchema LdapSchema { get; set; } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs similarity index 87% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs index 737c64d3..2f5433e6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/CheckConnectionRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapConnectionData.cs @@ -1,6 +1,6 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; -public class CheckConnectionRequest +public class LdapConnectionData { public string ConnectionString { get; set; } public string UserName { get; set; } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs deleted file mode 100644 index ab37c523..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadSchemaRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; - -namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; - -public class LoadSchemaRequest -{ - public string ConnectionString { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public int BindTimeoutInSeconds { get; set; } - - public static LoadSchemaRequest FromContext(RadiusPipelineContext context) - { - return new LoadSchemaRequest - { - ConnectionString = context.LdapConfiguration.ConnectionString, - UserName = context.LdapConfiguration.Username, - Password = context.LdapConfiguration.Password, - BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, - }; - } -} \ 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 index 8f170581..60e21c34 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LoadUserGroupRequest.cs @@ -5,10 +5,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public class LoadUserGroupRequest { - public string ConnectionString { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public int BindTimeoutInSeconds { get; set; } + public LdapConnectionData ConnectionData { get; set; } public ILdapSchema LdapSchema { get; set; } public DistinguishedName UserDN { get; set; } public DistinguishedName? SearchBase { get; set; } 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 index f22f3d0c..d23343ab 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs @@ -6,13 +6,9 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; public class MembershipRequest { - public string ConnectionString { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public int BindTimeoutInSeconds { get; set; } + public LdapConnectionData ConnectionData { get; set; } public ILdapSchema LdapSchema { get; set; } public DistinguishedName DistinguishedName { get; set; } - public DistinguishedName? SearchBase { get; set; } public DistinguishedName[] TargetGroups { get; set; } public DistinguishedName[] NestedGroupsBaseDns { get; set; } @@ -23,12 +19,15 @@ public static MembershipRequest FromContext(RadiusPipelineContext context, IRead return new MembershipRequest { - ConnectionString = context.LdapConfiguration.ConnectionString, - UserName = context.LdapConfiguration.Username, - Password = context.LdapConfiguration.Password, + 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, - BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, TargetGroups = groups.ToArray(), NestedGroupsBaseDns = context.LdapConfiguration.NestedGroupsBaseDns.ToArray() }; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs similarity index 78% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs index 1ee565f3..212ea1cc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/ILdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Ports/ILdapAdapter.cs @@ -1,7 +1,7 @@ using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; public interface ILdapAdapter { @@ -9,6 +9,6 @@ public interface ILdapAdapter bool IsMemberOf(MembershipRequest request); ILdapProfile? FindUserProfile(FindUserRequest request); bool ChangeUserPassword(ChangeUserPasswordRequest request); - ILdapSchema? LoadSchema(LoadSchemaRequest request); - bool CheckConnecion(CheckConnectionRequest 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 index acbe4b5f..47fa0db8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Exceptions/MultifactorApiUnreachableException.cs @@ -6,7 +6,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Exceptions { [Serializable] - internal class MultifactorApiUnreachableException : Exception + public class MultifactorApiUnreachableException : Exception { public MultifactorApiUnreachableException() { } public MultifactorApiUnreachableException(string message) : base(message) { } diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs index dbe3ffe4..0155d9fd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/AccessRequestQuery.cs @@ -9,6 +9,5 @@ public class AccessRequestQuery public string? PassCode { get; set; } public string? CallingStationId { get; set; } public string? CalledStationId { get; set; } - public bool InlineEnroll { get; set; } public string SignUpGroups { get; set; } } \ 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 index f3b71adf..4d373c4e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs @@ -41,7 +41,6 @@ public async Task CreateSecondFactorRequestAsync(RadiusPip _logger.LogWarning("Empty user name for second factor context. Request rejected."); return new SecondFactorResponse(AuthenticationStatus.Reject); } - // return new SecondFactorResponse(AuthenticationStatus.Bypass); if (_authenticatedClientCache.TryHitCache( personalData.CallingStationId, @@ -103,7 +102,9 @@ public async Task CreateSecondFactorRequestAsync(RadiusPip catch (MultifactorApiUnreachableException apiEx) { return ProcessMfException(apiEx, personalData.Identity, - context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, context.RequestPacket.RemoteEndpoint); + context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, + context.LdapConfiguration?.BypassSecondFactorWhenApiUnreachableGroups, + context.UserGroups, context.RequestPacket.RemoteEndpoint); } catch (Exception ex) { @@ -159,9 +160,10 @@ public async Task SendChallengeAsync(RadiusPipelineContext } catch (MultifactorApiUnreachableException apiEx) { - return ProcessMfException(apiEx, identity, + return ProcessMfException(apiEx, identity, context.ClientConfiguration.BypassSecondFactorWhenApiUnreachable, - context.RequestPacket.RemoteEndpoint); + context.LdapConfiguration?.BypassSecondFactorWhenApiUnreachableGroups, + context.UserGroups, context.RequestPacket.RemoteEndpoint); } catch (Exception ex) { @@ -232,10 +234,38 @@ private AccessRequestQuery CreateAccessRequestQuery(PersonalData personalData, R Email = personalData.Email, Phone = string.IsNullOrWhiteSpace(phone) ? personalData.Phone : phone, CalledStationId = personalData.CalledStationId, - CallingStationId = personalData.CallingStationId + 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) @@ -270,6 +300,8 @@ private SecondFactorResponse ProcessMfException( MultifactorApiUnreachableException apiEx, string identity, bool bypassSecondFactorWhenApiUnreachable, + IReadOnlyList bypassSecondFactorWhenApiUnreachableGroups, + HashSet userGroups, IPEndPoint remoteEndpoint) { _logger.LogError(apiEx, @@ -278,15 +310,18 @@ private SecondFactorResponse ProcessMfException( remoteEndpoint.Address, remoteEndpoint.Port, apiEx.Message); - - if (!bypassSecondFactorWhenApiUnreachable) + + if (bypassSecondFactorWhenApiUnreachable + && bypassSecondFactorWhenApiUnreachableGroups != null + && bypassSecondFactorWhenApiUnreachableGroups.Any() + && bypassSecondFactorWhenApiUnreachableGroups.Intersect(userGroups).Any()) { - var radCode = ConvertToAuthCode(null); - return new SecondFactorResponse(radCode); + var code = ConvertToAuthCode(AccessRequestResponse.Bypass); + return new SecondFactorResponse(code); } - - 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) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs index fff43416..b6d5da40 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs @@ -27,10 +27,10 @@ public static PersonalData ExtractPersonalData(RadiusPipelineContext context) public static string? GetSecondFactorIdentity(RadiusPipelineContext context) { - if (string.IsNullOrWhiteSpace(context.LdapConfiguration?.IdentityAttribute)) - return context.RequestPacket.UserName; - - return GetAttributeValue(context.LdapProfile?.Attributes, context.LdapConfiguration.IdentityAttribute); + 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) @@ -41,38 +41,16 @@ public static PersonalData ExtractPersonalData(RadiusPipelineContext context) foreach (var attribute in context.LdapProfile.Attributes) { - if (context.LdapConfiguration.PhoneAttributes.Contains(attribute.Name.Value)) + if (!context.LdapConfiguration.PhoneAttributes.Contains(attribute.Name.Value)) continue; + foreach (var value in attribute.GetNotEmptyValues()) { - foreach (var value in attribute.GetNotEmptyValues()) - { - if (!string.IsNullOrEmpty(value)) - return value; - } + if (!string.IsNullOrEmpty(value)) + return value; } } - return context.LdapProfile.Phone; } - private static string? GetAttributeValue(IReadOnlyCollection? attributes, string attributeName) - { - if (attributes == null) return null; - - foreach (var attr in attributes) - { - if (attr.Name == attributeName) - { - foreach (var value in attr.Values) - { - if (!string.IsNullOrWhiteSpace(value)) - return value; - } - } - } - - return null; - } - public static string? GetCallingStationId(string? callingStationIdAttributeValue, IPEndPoint remoteEndPoint) { return IPAddress.TryParse(callingStationIdAttributeValue ?? string.Empty, out _) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs index 5618fc7a..6c65b72f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChallengeProcessorProvider.cs @@ -1,4 +1,5 @@ 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; 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 index 387e871e..539b595e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs @@ -2,7 +2,9 @@ using Multifactor.Radius.Adapter.v2.Application.Cache; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -53,7 +55,7 @@ public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) public bool HasChallengeContext(ChallengeIdentifier identifier) => _cache.TryGetValue(identifier.RequestId, out _); - public async Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) + public Task ProcessChallengeAsync(ChallengeIdentifier identifier, RadiusPipelineContext context) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(context.LdapProfile); @@ -62,31 +64,34 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide var passwordChangeRequest = GetPasswordChangeRequest(identifier.RequestId); if (passwordChangeRequest == null) - return ChallengeStatus.Accept; + return Task.FromResult(ChallengeStatus.Accept); if (string.IsNullOrWhiteSpace(context.Passphrase.Raw)) { context.ResponseInformation.ReplyMessage = "Password is empty"; context.FirstFactorStatus = AuthenticationStatus.Reject; - return ChallengeStatus.Reject; + return Task.FromResult(ChallengeStatus.Reject); } if (string.IsNullOrWhiteSpace(passwordChangeRequest.NewPasswordEncryptedData)) - return RepeatPasswordChallenge(context, passwordChangeRequest); + return Task.FromResult(RepeatPasswordChallenge(context, passwordChangeRequest)); var decryptedNewPassword = ProtectionService.Unprotect(context.ClientConfiguration.MultifactorSharedSecret, passwordChangeRequest.NewPasswordEncryptedData); if (decryptedNewPassword != context.Passphrase.Raw) - return PasswordsNotMatchChallenge(context, passwordChangeRequest); + return Task.FromResult(PasswordsNotMatchChallenge(context, passwordChangeRequest)); var request = new ChangeUserPasswordRequest { - ConnectionString = context.LdapConfiguration.ConnectionString, - UserName = context.LdapConfiguration.Username, - Password = context.LdapConfiguration.Password, + 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, - BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds }; var success = _ldapAdapter.ChangeUserPassword(request); @@ -95,11 +100,11 @@ public async Task ProcessChallengeAsync(ChallengeIdentifier ide context.ResponseInformation.State = null; if (success) - return ChallengeStatus.Accept; + return Task.FromResult(ChallengeStatus.Accept); context.FirstFactorStatus = AuthenticationStatus.Reject; - return ChallengeStatus.Reject; + return Task.FromResult(ChallengeStatus.Reject); } private PasswordChangeCache? GetPasswordChangeRequest(string id) 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 index 38e6dbf4..df3c446c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessor.cs @@ -1,4 +1,5 @@ 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; 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 index 451807f9..fb4611ba 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/IChallengeProcessorProvider.cs @@ -1,4 +1,5 @@ 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs similarity index 79% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs index 88954dff..099d87bf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeStatus.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeStatus.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; public enum ChallengeStatus { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs similarity index 80% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs index 39ee904f..1a9d33d8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/ChallengeType.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/Models/Enums/ChallengeType.cs @@ -1,4 +1,4 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models.Enums; public enum ChallengeType { 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 index 3deb82f5..92ca11b8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index 7fd56119..9808f39c 100644 --- 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 @@ -13,8 +13,7 @@ public string FormatName(string userName, ILdapProfile ldapProfile) { var identity = new UserIdentity(userName); - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) return userName; return ldapProfile.Dn.StringRepresentation; 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 index 8dd857b7..cf5d100e 100644 --- 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 @@ -13,8 +13,7 @@ public string FormatName(string userName, ILdapProfile ldapProfile) { var identity = new UserIdentity(userName); - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) return userName; return ldapProfile.Dn.StringRepresentation; 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 index 331a6b9f..6ca23792 100644 --- 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 @@ -13,8 +13,7 @@ public string FormatName(string userName, ILdapProfile ldapProfile) { var identity = new UserIdentity(userName); - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) return userName; return ldapProfile.Dn.StringRepresentation; 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 index 5563bf82..cd4c522c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs @@ -4,6 +4,7 @@ using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -95,7 +96,7 @@ private bool ValidateUserCredentials( bindName = string.IsNullOrWhiteSpace(formatted) ? login : formatted; _logger.LogDebug("Use '{name}' for LDAP bind.", bindName); - var request = new CheckConnectionRequest + var request = new LdapConnectionData { ConnectionString = serverConfig.ConnectionString, UserName = bindName, @@ -103,7 +104,7 @@ private bool ValidateUserCredentials( BindTimeoutInSeconds = serverConfig.BindTimeoutSeconds }; - return _ldapAdapter.CheckConnecion(request); + return _ldapAdapter.CheckConnection(request); } catch (Exception ex) { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs similarity index 93% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs index 50b5c958..00723495 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs @@ -1,6 +1,6 @@ using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; public interface IPipelineProvider { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs similarity index 92% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs index 18fb5cd3..72b1ecd0 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipeline.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipeline.cs @@ -1,6 +1,6 @@ using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; public interface IRadiusPipeline { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs similarity index 93% rename from src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs index 93fd2679..4f1026fe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/IRadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs @@ -1,6 +1,6 @@ using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; public interface IRadiusPipelineFactory { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs similarity index 60% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs index 5f46788b..c7d659d1 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipeline.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs @@ -1,24 +1,20 @@ -using Microsoft.Extensions.Logging; -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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; public class RadiusPipeline : IRadiusPipeline { private readonly List _steps; - // private readonly ILogger _logger; public RadiusPipeline(List steps) { _steps = steps ?? throw new ArgumentNullException(nameof(steps)); - // _logger = logger ?? throw new ArgumentNullException(nameof(logger)); TODO fix or return } public async Task ExecuteAsync(RadiusPipelineContext context) { - // _logger.LogDebug("Starting pipeline execution with {StepCount} steps", _steps.Count); foreach (var step in _steps) { @@ -26,8 +22,6 @@ public async Task ExecuteAsync(RadiusPipelineContext context) if (context.IsTerminated) { - // _logger.LogDebug("Pipeline terminated early at step {StepName}", - // step.GetType().Name); break; } } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs similarity index 97% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs index 52872747..379f363f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs @@ -3,10 +3,10 @@ using Microsoft.Extensions.Logging; 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.Infrastructure.Pipeline; +namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; public class RadiusPipelineFactory : IRadiusPipelineFactory { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs similarity index 78% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs index 90b20baf..f718a7fe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Pipeline/RadiusPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs @@ -1,25 +1,22 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; +using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; +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(); - private readonly ServiceConfiguration _serviceConfiguration; public RadiusPipelineProvider( IRadiusPipelineFactory pipelineFactory, - ILogger logger, - ServiceConfiguration serviceConfiguration) + ILogger logger) { _pipelineFactory = pipelineFactory; _logger = logger; - _serviceConfiguration = serviceConfiguration; } public IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs index 5b049f10..b66de84a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessChallengeStep.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; 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.Application.Features.Pipeline.Steps; 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 index 2821d3b9..4b9e93b6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs index 9f254cda..9807e650 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/FirstFactorStep.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; 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.FirstFactor; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; 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 index 0786e515..53390308 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs @@ -3,6 +3,7 @@ using Multifactor.Radius.Adapter.v2.Application.Cache; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -47,7 +48,13 @@ public Task ExecuteAsync(RadiusPipelineContext context) return schema; } - var request = LoadSchemaRequest.FromContext(context); + 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) 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 index 90835ed9..2d247e2d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/PreAuthCheckStep.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; 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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; @@ -8,18 +10,22 @@ 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) + public PreAuthCheckStep(ILogger logger, ILdapAdapter ldapAdapter) { - _logger = logger; + _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 context.Passphrase?.Otp == null: + 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, @@ -38,4 +44,19 @@ public Task ExecuteAsync(RadiusPipelineContext context) 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.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs index 95c2a1fc..bcd1c78d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs @@ -4,6 +4,7 @@ using Multifactor.Radius.Adapter.v2.Application.Cache; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -73,10 +74,13 @@ public Task ExecuteAsync(RadiusPipelineContext context) _logger.LogInformation("Try to find '{userIdentity}' profile at '{domain}'.", userIdentity.Identity, domain.StringRepresentation); var request = new FindUserRequest { - ConnectionString = context.LdapConfiguration.ConnectionString, - UserName = context.LdapConfiguration.Username, - Password = context.LdapConfiguration.Password, - BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, + 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, @@ -87,7 +91,7 @@ public Task ExecuteAsync(RadiusPipelineContext context) if (profile is null) return profile; - var expirationDate = DateTimeOffset.Now.AddHours(0); //context.LdapConfiguration!.UserProfileCacheLifeTimeInHours = 0 ???? + 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()); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs index 0d6c758c..d557e221 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs @@ -2,10 +2,11 @@ using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; +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; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs index b8dac41b..7738acfa 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/StatusServerFilteringStep.cs @@ -27,7 +27,7 @@ public async Task ExecuteAsync(RadiusPipelineContext context) } var uptime = _applicationVariables.UpTime; - context.ResponseInformation.ReplyMessage = $"Server up {uptime.Days} days {uptime:hh\\:mm\\:ss}, ver.: {_applicationVariables.AppVersion}"; + 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(); 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 index a76b9b74..e364a7e8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -61,11 +62,14 @@ private void LoadUserGroupsFromContainers(RadiusPipelineContext context, HashSet _logger.LogDebug("Loading nested groups from '{dn}' at '{domain}' for '{user}'", dn, context.LdapConfiguration.ConnectionString, context.RequestPacket.UserName); var request = new LoadUserGroupRequest - { - ConnectionString = context.LdapConfiguration.ConnectionString, - UserName = context.LdapConfiguration.Username, - Password = context.LdapConfiguration.Password, - BindTimeoutInSeconds = context.LdapConfiguration.BindTimeoutSeconds, + { + 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 @@ -82,15 +86,18 @@ private void LoadUserGroupsFromContainers(RadiusPipelineContext context, HashSet private void LoadUserGroupsFromRoot(RadiusPipelineContext context, HashSet userGroups) { - var request = new LoadUserGroupRequest + 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 - }; + 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); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs index e83966d7..18978814 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs @@ -44,7 +44,7 @@ public Task ExecuteAsync(RadiusPipelineContext context) if (identity.Format != UserIdentityFormat.UserPrincipalName) return Task.CompletedTask; - if (!IsPermittedSuffix(identity.GetUpnSuffix(), serverSettings.IncludedDomains, serverSettings.ExcludedDomains)) + 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); @@ -53,15 +53,15 @@ public Task ExecuteAsync(RadiusPipelineContext context) return Task.CompletedTask; } - private static bool IsPermittedSuffix(string domain, IReadOnlyList includedDomains, IReadOnlyList excludedDomains) + private static bool IsPermittedSuffix(string domain, IReadOnlyList includedSuffixes, IReadOnlyList excludedSuffixes) { if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain)); - if (includedDomains.Count > 0) - return includedDomains.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); + if (includedSuffixes.Count > 0) + return includedSuffixes.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); - if (excludedDomains.Count > 0) - return excludedDomains.All(excluded => !excluded.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); + if (excludedSuffixes.Count > 0) + return excludedSuffixes.All(excluded => !excluded.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); return true; } @@ -70,7 +70,7 @@ private static void TerminateWithError(RadiusPipelineContext context, string rep { context.FirstFactorStatus = AuthenticationStatus.Reject; context.SecondFactorStatus = AuthenticationStatus.Awaiting; - context.Terminate(); context.ResponseInformation.ReplyMessage = replyMessage; + context.Terminate(); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs deleted file mode 100644 index b7596ec9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/IResponseInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; - -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.Application/Features/Radius/Ports/IUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs index b9f23413..98587eea 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Ports/IUdpClient.cs @@ -5,7 +5,6 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; public interface IUdpClient : IDisposable { - Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint, - CancellationToken cancellationToken = default); + Task SendAsync(byte[] datagram, int bytesCount, IPEndPoint endPoint); Task ReceiveAsync(CancellationToken cancellationToken = default); } \ 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 index 29df2fdd..a7a83810 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; 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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; @@ -102,6 +103,8 @@ private async Task ExecutePipeline( try { + var logMessage = $"Start executing pipeline for '{clientConfiguration.Name}'" + (ldapServerConfiguration is not null ? $" at '{ldapServerConfiguration.ConnectionString}'" : string.Empty); + _logger.LogDebug(logMessage); await pipeline.ExecuteAsync(context); var responseRequest = SendAdapterResponseRequest.FromContext(context); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs index f5c79195..c6579fa5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/ProtectionService.cs @@ -23,9 +23,12 @@ public static string Unprotect(string secret, string data) ArgumentException.ThrowIfNullOrWhiteSpace(data, nameof(data)); var bytes = FromBase64(data); - if (!OperatingSystem.IsWindows()) return BytesToString(bytes); - var additionalEntropy = StringToBytes(secret); - return BytesToString(ProtectedData.Unprotect(bytes, additionalEntropy, DataProtectionScope.CurrentUser)); + if (OperatingSystem.IsWindows()) + { + var additionalEntropy = StringToBytes(secret); + return BytesToString(ProtectedData.Unprotect(bytes, additionalEntropy, DataProtectionScope.CurrentUser)); + } + return BytesToString(bytes); } private static byte[] StringToBytes(string s) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs index 83466169..0c4a61da 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Security/RadiusPasswordProtector.cs @@ -34,9 +34,7 @@ private static byte[] CreateKey(SharedSecret sharedSecret, RadiusAuthenticator a 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); - - using var md5 = MD5.Create(); - return md5.ComputeHash(key); + return MD5.HashData(key); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs index 8367b2e7..6942b61c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs @@ -13,11 +13,6 @@ public CustomLdapConnectionFactory() { _factory = LdapConnectionFactory.Create(); } - - public CustomLdapConnectionFactory(IEnumerable ldapConnectionFactories) - { - _factory = new LdapConnectionFactory (NullLogger.Instance, ldapConnectionFactories); - } public ILdapConnection CreateConnection(LdapConnectionOptions ldapConnectionOptions) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs index 016cf2f3..e4ff283e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -11,6 +11,7 @@ using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -38,12 +39,7 @@ public LdapAdapter( public IReadOnlyList LoadUserGroups(LoadUserGroupRequest request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString, true), - AuthType.Basic, - request.UserName, - request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - using var connection = _connectionFactory.CreateConnection(options); + 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(); @@ -52,13 +48,7 @@ public IReadOnlyList LoadUserGroups(LoadUserGroupRequest request) #region FindUserProfile public ILdapProfile? FindUserProfile(FindUserRequest request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), - AuthType.Basic, - request.UserName, - request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - using var connection = _connectionFactory.CreateConnection(options); - + using var connection = CreateConnection(request.ConnectionData); var identityToSearch = request.UserIdentity; if (request.UserIdentity.Format == UserIdentityFormat.NetBiosName) { @@ -92,7 +82,7 @@ private string GetFilter(UserIdentity identity, ILdapSchema schema) }; #endregion - public ILdapSchema? LoadSchema(LoadSchemaRequest request) + public ILdapSchema? LoadSchema(LdapConnectionData request) { var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), AuthType.Basic, @@ -102,15 +92,10 @@ private string GetFilter(UserIdentity identity, ILdapSchema schema) return _schemaLoader.Load(options); } - public bool CheckConnecion(CheckConnectionRequest request) + public bool CheckConnection(LdapConnectionData request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), - AuthType.Basic, - request.UserName, - request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - using var connection = _connectionFactory.CreateConnection(options); - return true; + using var connection = CreateConnection(request); + return true; //true of exception } #region IsMemberOf @@ -119,12 +104,7 @@ public bool IsMemberOf(MembershipRequest request) ArgumentNullException.ThrowIfNull(request); if(request.TargetGroups == null || request.TargetGroups.Length == 0) throw new InvalidOperationException(); - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), - AuthType.Basic, - request.UserName, - request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - var connection = _connectionFactory.CreateConnection(options); + using var connection = CreateConnection(request.ConnectionData); return request.NestedGroupsBaseDns.Length > 0 ? request.NestedGroupsBaseDns @@ -145,14 +125,9 @@ public bool ChangeUserPassword(ChangeUserPasswordRequest request) { ArgumentNullException.ThrowIfNull(request, nameof(request)); - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), - AuthType.Basic, - request.UserName, - request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); - using var connection = _connectionFactory.CreateConnection(options); - var changePasswordrequest = BuildPasswordChangeRequest(request.LdapSchema, request.DistinguishedName, request.NewPassword); - var response = connection.SendRequest(changePasswordrequest); + 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; } @@ -176,4 +151,14 @@ private static ModifyRequest BuildPasswordChangeRequest(ILdapSchema ldapSchema, } #endregion + private ILdapConnection CreateConnection(LdapConnectionData data) + { + var options = new LdapConnectionOptions(new LdapConnectionString(data.ConnectionString, true), + 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.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs index 975447df..392ec423 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs @@ -24,7 +24,7 @@ public static AccessRequestDto FromQuery(AccessRequestQuery query) Phone = query.Phone, PassCode = query.PassCode, CalledStationId = query.CalledStationId, - Capabilities = new Capabilities{ InlineEnroll = query.InlineEnroll }, + Capabilities = new Capabilities{ InlineEnroll = true }, GroupPolicyPreset = new GroupPolicyPreset{ SignUpGroups = query.SignUpGroups } }; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs index 291c9a1a..2d68bfa2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs @@ -1,8 +1,11 @@ +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; @@ -11,59 +14,174 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Multifactor; public class MultifactorApi : IMultifactorApi { - private const string _clientName = "multifactor-api"; - private readonly JsonSerializerOptions _options = new(JsonSerializerDefaults.Web); + 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, + + public MultifactorApi( + IHttpClientFactory clientFactory, ILogger logger) { _clientFactory = clientFactory; _logger = logger; } - public async Task CreateAccessRequest(AccessRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken) + public async Task CreateAccessRequest( + AccessRequestQuery query, + MultifactorAuthData authData, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query, nameof(query)); var dto = AccessRequestDto.FromQuery(query); - var client = _clientFactory.CreateClient(_clientName); - var headerValue = new BasicAuthHeaderValue(authData.ApiKey, authData.ApiSecret); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", headerValue.GetBase64()); - var response = await client.PostAsJsonAsync("access/requests/ra", dto, cancellationToken: cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var errContent = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogError("Multifactor API request was unsuccessful. Method: access/requests/ra. Reason: {error:l}", errContent); - throw new Exception("Error while requesting access/requests/ra"); - } - response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(cancellationToken); //TODO add error cathcher - Console.WriteLine(content); - var accessResponse = JsonSerializer.Deserialize>(content, _options); - return accessResponse.Model; + return await SendRequestAsync( + endpoint: "access/requests/ra", + data: dto, + authData: authData, + cancellationToken: cancellationToken); } - public async Task SendChallengeAsync(ChallengeRequestQuery query, MultifactorAuthData authData, CancellationToken cancellationToken) + public async Task SendChallengeAsync( + ChallengeRequestQuery query, + MultifactorAuthData authData, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query, nameof(query)); var dto = ChallengeRequestDto.FromQuery(query); - var client = _clientFactory.CreateClient(_clientName); - var headerValue = new BasicAuthHeaderValue(authData.ApiKey, authData.ApiSecret); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", headerValue.GetBase64()); - var response = await client.PostAsJsonAsync("access/requests/ra/challenge", dto, cancellationToken: cancellationToken); + 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 errContent = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogError("Multifactor API request was unsuccessful. Method: access/requests/ra/challenge: {error:l}", errContent); - throw new Exception("Error while requesting access/requests/ra/challenge"); + 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(); } - response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); - var challengeResponse = JsonSerializer.Deserialize>(content, _options); - return challengeResponse.Model; + + 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.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs index 2b74c8b3..9690673f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs @@ -54,6 +54,7 @@ public async Task Handle(UdpReceiveResult udpPacket) } var requestPacket = _radiusPacketService.ParsePacket(payload, new SharedSecret(clientConfiguration.RadiusSharedSecret)); + Console.WriteLine(Encoding.UTF8.GetString(requestPacket.Authenticator.Value)); requestPacket.ProxyEndpoint = proxyEndpoint; requestPacket.RemoteEndpoint = remoteEndpoint; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs index a3e2c471..d6189a96 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Multifactor.Core.Ldap.LangFeatures; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; @@ -11,38 +10,19 @@ public sealed class CustomUdpClient : IUdpClient { private readonly UdpClient _udpClient; private readonly ILogger _logger; - private readonly UdpClientOptions _options; public CustomUdpClient( IPEndPoint endPoint, - ILogger logger, - IOptions options) + ILogger logger) { Throw.IfNull(endPoint, nameof(endPoint)); _logger = logger; - _options = options?.Value ?? new UdpClientOptions(); _udpClient = new UdpClient(); - ConfigureSocket(endPoint); _logger.LogInformation("UDP client initialized on {Endpoint}", endPoint); } - private void ConfigureSocket(IPEndPoint endPoint) - { - //TODO обоснования и дефолтные значения - var socket = _udpClient.Client; - - socket.ReceiveBufferSize = _options.ReceiveBufferSize; - socket.SendBufferSize = _options.SendBufferSize; - socket.ReceiveTimeout = _options.ReceiveTimeoutMs; - socket.Ttl = _options.Ttl; - socket.DontFragment = true; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(endPoint); - // socket.NoDelay = true; - } - public async Task ReceiveAsync(CancellationToken cancellationToken = default) { try @@ -67,12 +47,10 @@ public async Task ReceiveAsync(CancellationToken cancellationT public async Task SendAsync( byte[] datagram, int bytesCount, - IPEndPoint endPoint, - CancellationToken cancellationToken = default) + IPEndPoint endPoint) { - if (datagram == null) - throw new ArgumentNullException(nameof(datagram)); - + ArgumentNullException.ThrowIfNull(datagram); + if (bytesCount <= 0 || bytesCount > datagram.Length) throw new ArgumentOutOfRangeException(nameof(bytesCount)); @@ -84,7 +62,7 @@ public async Task SendAsync( try { - await _udpClient.SendAsync(datagram, bytesCount, endPoint); //TODO разобраться async хочется + await _udpClient.SendAsync(datagram, bytesCount, endPoint); return bytesCount; } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.MessageSize) @@ -108,22 +86,13 @@ public void Dispose() { try { - _udpClient?.Close(); - _udpClient?.Dispose(); - _logger?.LogDebug("UDP client disposed"); + _udpClient.Close(); + _udpClient.Dispose(); + _logger.LogDebug("UDP client disposed"); } catch (Exception ex) { - _logger?.LogWarning(ex, "Error disposing UDP client"); + _logger.LogWarning(ex, "Error disposing UDP client"); } } } - -//TODO Maybe move to config -public class UdpClientOptions -{ - public int ReceiveBufferSize { get; set; } = 64 * 1024; // 64KB - public int SendBufferSize { get; set; } = 64 * 1024; // 64KB - public int ReceiveTimeoutMs { get; set; } = 0; - public short Ttl { get; set; } = 30; -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs index e4fd5bc3..1afb71d3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Cache/AuthenticatedClientCache/AuthenticatedClient.cs @@ -7,14 +7,6 @@ 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 AuthenticatedClient(params string?[] components) { ArgumentNullException.ThrowIfNull(components); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs index d931eafe..54688421 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -18,22 +18,18 @@ public ConfigurationLoader( public ServiceConfiguration Load() { - return LoadAsync(CancellationToken.None).GetAwaiter().GetResult(); + return Task.Run(() => LoadAsync(CancellationToken.None)).GetAwaiter().GetResult(); } public async Task LoadAsync(CancellationToken cancellationToken) { - var rootConfig = await LoadRootConfigurationAsync(cancellationToken); - var clients = await LoadClientConfigurationsAsync(cancellationToken); - var serviceConfig = new ServiceConfiguration { RootConfiguration = rootConfig, ClientsConfigurations = clients }; - return serviceConfig; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs index 89d64da3..f6ca2448 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Text.RegularExpressions; using System.Xml.Linq; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; @@ -13,6 +14,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; public class XmlConfigurationParser : IConfigurationParser { private readonly IRadiusDictionary _dictionary; + const string _commonPrefix = "RAD_"; public XmlConfigurationParser( IRadiusDictionary dictionary) @@ -23,60 +25,60 @@ public XmlConfigurationParser( public async Task ParseRootConfigAsync(string filePath, CancellationToken ct) { var xml = await XmlReader.ReadAsync(filePath, ct); - var settings = XmlReader.ExtractAppSettings(xml); + var settingsXml = XmlReader.ExtractAppSettings(xml); + + var settingsEnv = EnvironmentReader.ReadEnvironments("RAD_APPSETTINGS"); return new RootConfiguration { - MultifactorApiUrls = ValueParser.ParseUrls(settings.GetValueOrDefault("multifactor-api-url"), required: true), - MultifactorApiProxy = settings.GetValueOrDefault("multifactor-api-proxy"), - MultifactorApiTimeout = ValueParser.ParseTimeout(settings.GetValueOrDefault("multifactor-api-timeout"), + MultifactorApiUrls = ValueParser.ParseUrls(GetRawValue("multifactor-api-url", settingsEnv, settingsXml, true), required: true), + MultifactorApiProxy = GetRawValue("multifactor-api-proxy", settingsEnv, settingsXml), + MultifactorApiTimeout = ValueParser.ParseTimeout(GetRawValue("multifactor-api-timeout", settingsEnv, settingsXml), TimeSpan.FromSeconds(65)), - AdapterServerEndpoint = ValueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-server-endpoint"), required: true), - LoggingFormat = settings.GetValueOrDefault("logging-format") ?? string.Empty, - SyslogUseTls = ValueParser.ParseBool("syslog-use-tls", false), - SyslogServer = settings.GetValueOrDefault("syslog-server") ?? string.Empty, - SyslogFormat = settings.GetValueOrDefault("syslog-format") ?? string.Empty, - SyslogFacility = settings.GetValueOrDefault("syslog-facility") ?? string.Empty, - SyslogAppName = settings.GetValueOrDefault("syslog-app-name") ?? "multifactor-radius", - SyslogFramer = settings.GetValueOrDefault("syslog-framer") ?? string.Empty, - SyslogOutputTemplate = settings.GetValueOrDefault("syslog-output-template") ?? string.Empty, - ConsoleLogOutputTemplate = settings.GetValueOrDefault("console-log-output-template") ?? string.Empty, - FileLogOutputTemplate = settings.GetValueOrDefault("file-log-output-template") ?? string.Empty, - LogFileMaxSizeBytes = ValueParser.ParseInt("log-file-max-size-bytes", 1073741824), - LoggingLevel = settings.GetValueOrDefault("logging-level"), + AdapterServerEndpoint = ValueParser.ParseEndpoint(GetRawValue("adapter-server-endpoint", settingsEnv, settingsXml, true), required: true), + LoggingFormat = GetRawValue("logging-format", settingsEnv, settingsXml), + SyslogUseTls = ValueParser.ParseBool(GetRawValue("syslog-use-tls", settingsEnv, settingsXml), false), + SyslogServer = GetRawValue("syslog-server", settingsEnv, settingsXml), + SyslogFormat = GetRawValue("syslog-format", settingsEnv, settingsXml), + SyslogFacility = GetRawValue("syslog-facility", settingsEnv, settingsXml), + SyslogAppName = GetRawValue("syslog-app-name", settingsEnv, settingsXml) ?? "multifactor-radius", + SyslogFramer = GetRawValue("syslog-framer", settingsEnv, settingsXml), + SyslogOutputTemplate = GetRawValue("syslog-output-template", settingsEnv, settingsXml), + ConsoleLogOutputTemplate = GetRawValue("console-log-output-template", settingsEnv, settingsXml), + FileLogOutputTemplate = GetRawValue("file-log-output-template", settingsEnv, settingsXml), + LogFileMaxSizeBytes = ValueParser.ParseInt(GetRawValue("log-file-max-size-bytes", settingsEnv, settingsXml), 1073741824), + LoggingLevel = GetRawValue("logging-level", settingsEnv, settingsXml) }; } public async Task ParseClientConfigAsync(string filePath, CancellationToken ct) { var xml = await XmlReader.ReadAsync(filePath, ct); - var settings = XmlReader.ExtractAppSettings(xml); + var settingsXml = XmlReader.ExtractAppSettings(xml); + + var prefix = TransformName(filePath); + var settingsEnv = EnvironmentReader.ReadEnvironments($"{_commonPrefix}{prefix}APPSETTINGS"); var dto = new ClientConfiguration { Name = Path.GetFileNameWithoutExtension(filePath), - MultifactorNasIdentifier = settings.GetValueOrDefault("multifactor-nas-identifier") - ?? throw new InvalidConfigurationException("multifactor-nas-identifier is required"), - MultifactorSharedSecret = settings.GetValueOrDefault("multifactor-shared-secret") - ?? throw new InvalidConfigurationException("multifactor-shared-secret is required"), - SignUpGroups = ValueParser.ParseStringList(settings.GetValueOrDefault("sign-up-group")), - BypassSecondFactorWhenApiUnreachable = ValueParser.ParseBool(settings.GetValueOrDefault("bypass-second-factor-when-api-unreachable"), true), - FirstFactorAuthenticationSource = ValueParser.ParseEnum(settings.GetValueOrDefault("first-factor-authentication-source"), required: true), - AdapterClientEndpoint = ValueParser.ParseEndpoint(settings.GetValueOrDefault("adapter-client-endpoint"), required: true), - RadiusClientIp = ValueParser.ParseIpAddress(settings.GetValueOrDefault("radius-client-ip")), - RadiusClientNasIdentifier = settings.GetValueOrDefault("radius-client-nas-identifier") ?? string.Empty, - RadiusSharedSecret = settings.GetValueOrDefault("radius-shared-secret") - ?? throw new InvalidConfigurationException("radius-shared-secret is required"), - NpsServerEndpoints = ValueParser.ParseEndpoints(settings.GetValueOrDefault("nps-server-endpoint"), required: true), - NpsServerTimeout = ValueParser.ParseTimeout(settings.GetValueOrDefault("nps-server-timeout"), TimeSpan.Parse("00:00:05")), - Privacy = ValueParser.ParsePrivacyModeWithFields(settings.GetValueOrDefault("privacy-mode")), - PreAuthenticationMethod = ValueParser.ParseEnum(settings.GetValueOrDefault("pre-authentication-method"), PreAuthMode.None), - AuthenticationCacheLifetime = ValueParser.ParseTimeSpan( - settings.GetValueOrDefault("authentication-cache-lifetime")), - CallingStationIdAttribute = settings.GetValueOrDefault("calling-station-id-attribute"), - IpWhiteList = ValueParser.ParseIpRanges( - settings.GetValueOrDefault("ip-white-list")), - InvalidCredentialDelay = ValueParser.ParseDelaySettings(settings.GetValueOrDefault("invalid-credential-delay")), + MultifactorNasIdentifier = GetRawValue("multifactor-nas-identifier", settingsEnv, settingsXml, true), + MultifactorSharedSecret = GetRawValue("multifactor-shared-secret", settingsEnv, settingsXml, true), + SignUpGroups = ValueParser.ParseStringList(GetRawValue("sign-up-group", settingsEnv, settingsXml)), + BypassSecondFactorWhenApiUnreachable = ValueParser.ParseBool(GetRawValue("bypass-second-factor-when-api-unreachable", settingsEnv, settingsXml), true), + FirstFactorAuthenticationSource = ValueParser.ParseEnum(GetRawValue("first-factor-authentication-source", settingsEnv, settingsXml, true), required: true), + AdapterClientEndpoint = ValueParser.ParseEndpoint(GetRawValue("adapter-client-endpoint", settingsEnv, settingsXml, true), required: true), + RadiusClientIp = ValueParser.ParseIpAddress(GetRawValue("radius-client-ip", settingsEnv, settingsXml)), + RadiusClientNasIdentifier = GetRawValue("radius-client-nas-identifier", settingsEnv, settingsXml), + RadiusSharedSecret = GetRawValue("radius-shared-secret", settingsEnv, settingsXml, true), + NpsServerEndpoints = ValueParser.ParseEndpoints(GetRawValue("nps-server-endpoint", settingsEnv, settingsXml, true), required: true), + NpsServerTimeout = ValueParser.ParseTimeout(GetRawValue("nps-server-timeout", settingsEnv, settingsXml), TimeSpan.Parse("00:00:05")), + Privacy = ValueParser.ParsePrivacyModeWithFields(GetRawValue("privacy-mode", settingsEnv, settingsXml)), + PreAuthenticationMethod = ValueParser.ParseEnum(GetRawValue("pre-authentication-method", settingsEnv, settingsXml), PreAuthMode.None), + AuthenticationCacheLifetime = ValueParser.ParseTimeSpan(GetRawValue("authentication-cache-lifetime", settingsEnv, settingsXml)), + CallingStationIdAttribute = GetRawValue("calling-station-id-attribute", settingsEnv, settingsXml), + IpWhiteList = ValueParser.ParseIpRanges(GetRawValue("ip-white-list", settingsEnv, settingsXml)), + InvalidCredentialDelay = ValueParser.ParseDelaySettings(GetRawValue("invalid-credential-delay", settingsEnv, settingsXml)), ReplyAttributes = ParseReplyAttributes(xml) }; @@ -90,7 +92,7 @@ private List ParseLdapServers(XDocument xml, string con var servers = new List(); var ldapElements = XmlReader.GetLdapServerElements(xml); - if (ldapElements == null) + if (ldapElements == null || ldapElements.Count == 0) return servers; servers.AddRange(ldapElements.Select(element => new LdapServerConfiguration @@ -113,7 +115,8 @@ private List ParseLdapServers(XDocument xml, string con IncludedDomains = ValueParser.ParseStringList(element.Attribute("included-domains")?.Value), ExcludedDomains = ValueParser.ParseStringList(element.Attribute("excluded-domains")?.Value), IncludedSuffixes = ValueParser.ParseStringList(element.Attribute("included-suffixes")?.Value), - ExcludedSuffixes = ValueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value) + ExcludedSuffixes = ValueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value), + BypassSecondFactorWhenApiUnreachableGroups = ValueParser.ParseStringList(element.Attribute("bypass-second-factor-when-api-unreachable-groups")?.Value) })); return servers; @@ -200,4 +203,21 @@ private object ParseRadiusReplyValue(string attributeName, string value) _ => throw new Exception($"Unknown type {attribute.Type}") }; } + + private static string TransformName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + name = Regex.Replace(name, @"\s+", string.Empty); + return name; + } + + private static string GetRawValue(string key, IReadOnlyDictionary primary, IReadOnlyDictionary secondary, bool required = false) + { + return primary.TryGetValue(key, out var primaryValue) ? primaryValue + : secondary.TryGetValue(key, out var secondaryValue) ? secondaryValue + : !required ? string.Empty : throw new InvalidConfigurationException($"{key} is required") ; + } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs new file mode 100644 index 00000000..b22426d2 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs @@ -0,0 +1,14 @@ +using System.Collections; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public static class EnvironmentReader +{ + public static IReadOnlyDictionary ReadEnvironments(string? prefix = null) + { + return Environment.GetEnvironmentVariables() + .Cast() + .Where(x => x.Key.ToString().StartsWith(prefix)) + .ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs index 21d7533b..c0db1a5f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs @@ -11,7 +11,6 @@ public static async Task ReadAsync(string filePath, CancellationToken { try { - await using var stream = File.OpenRead(filePath); var settings = new XmlReaderSettings { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs index ef447901..6de0688d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -1,7 +1,6 @@ using System.Security.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; using Multifactor.Core.Ldap.LdapGroup.Load; using Multifactor.Core.Ldap.LdapGroup.Membership; @@ -9,9 +8,9 @@ using Multifactor.Radius.Adapter.v2.Application.Cache; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; +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.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge; 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; @@ -24,9 +23,7 @@ using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; @@ -68,9 +65,8 @@ public static void AddRadiusUdpClient(this IServiceCollection services) var config = serviceProvider.GetRequiredService(); var endpoint = config.RootConfiguration.AdapterServerEndpoint; var logger = serviceProvider.GetService>(); - var options = serviceProvider.GetService>(); - return new CustomUdpClient(endpoint, logger, options); + return new CustomUdpClient(endpoint, logger); }); services.AddSingleton(); @@ -93,6 +89,7 @@ public static void AddMultifactorApi(this IServiceCollection services) { var primaryUrl = config.RootConfiguration.MultifactorApiUrls[0]; client.BaseAddress = primaryUrl; + client.Timeout = config.RootConfiguration.MultifactorApiTimeout; } }) .AddPolicyHandler((serviceProvider, request) => @@ -179,14 +176,7 @@ public static void AddInfraServices(this IServiceCollection services) services.AddSingleton(); services.AddTransient(); services.AddTransient(); - } - - public static void AddPipelines(this IServiceCollection services) - { - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } + } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index 8512eaab..402d81fe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -27,7 +27,8 @@ public static ILogger CreateLogger(RootConfiguration rootConfiguration) ConfigureLogging( loggerConfiguration, rootConfiguration.LoggingFormat, - rootConfiguration.SyslogOutputTemplate, + rootConfiguration.FileLogOutputTemplate, + rootConfiguration.LogFileMaxSizeBytes, rootConfiguration.ConsoleLogOutputTemplate); ConfigureSyslog(loggerConfiguration, rootConfiguration.SyslogServer, @@ -56,6 +57,7 @@ private static void ConfigureLogging( LoggerConfiguration loggerConfiguration, string? loggingFormat, string? fileTemplate, + int? fileSize, string? consoleTemplate) { var adapterPath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); @@ -68,7 +70,8 @@ private static void ConfigureLogging( .WriteTo.File(formatter, logsPath, flushToDiskInterval: TimeSpan.FromSeconds(1), - rollingInterval: RollingInterval.Day) ; + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: fileSize) ; if (!string.IsNullOrWhiteSpace(fileTemplate)) { @@ -84,7 +87,6 @@ private static void ConfigureLogging( if (!string.IsNullOrWhiteSpace(consoleTemplate)) loggerConfiguration.WriteTo.Console(outputTemplate: consoleTemplate); else - // loggerConfiguration.WriteTo.Console();TODO remove loggerConfiguration.WriteTo.Console( outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}", theme: AnsiConsoleTheme.Code, @@ -174,24 +176,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/Radius/Builders/RadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs index 10728cfa..10005f24 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -1,7 +1,10 @@ +using System.Net; +using System.Text; 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.Configurations.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; @@ -12,6 +15,22 @@ public class RadiusPacketBuilder : IRadiusPacketBuilder private readonly IRadiusCryptoProvider _cryptoProvider; private readonly ILogger _logger; private readonly IRadiusAttributeSerializer _attributeSerializer; + + + /// + /// 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, @@ -27,51 +46,26 @@ public RadiusPacketBuilder( public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (sharedSecret == null) throw new ArgumentNullException(nameof(sharedSecret)); + ArgumentNullException.ThrowIfNull(packet); + ArgumentNullException.ThrowIfNull(sharedSecret); - var packetBytes = new List(); - - // Header: Code (1), Identifier (1), Length (2), Authenticator (16) - packetBytes.Add((byte)packet.Code); - packetBytes.Add(packet.Identifier); - packetBytes.AddRange(new byte[2]); // Placeholder for length - packetBytes.AddRange(new byte[16]); // Placeholder for authenticator + var packetBytes = new List + { + // Header: Code (1), Identifier (1), Length (2), Authenticator (16) + (byte)packet.Code, + packet.Identifier + }; - int messageAuthenticatorPosition = -1; + packetBytes.AddRange(new byte[18]); // Placeholder for length and authenticator // Serialize attributes - foreach (var attribute in packet.Attributes.Values) - { - foreach (var value in attribute.Values) - { - var attributeBytes = _attributeSerializer.Serialize( - attribute.Name, - value, - packet.Authenticator, - sharedSecret, - packet.RequestAuthenticator); - - if (attributeBytes != null) - { - // Check if this is Message-Authenticator - var attributeDefinition = _radiusDictionary.GetAttribute(attribute.Name); - if (attributeDefinition?.Code == 80) // Message-Authenticator - { - messageAuthenticatorPosition = packetBytes.Count; - } - - packetBytes.AddRange(attributeBytes); - } - } - } + FillAttributes(packetBytes, packet.Authenticator, sharedSecret, packet.Attributes.Values, out int messageAuthenticatorPosition); // Set packet length ushort packetLength = (ushort)packetBytes.Count; var lengthBytes = BitConverter.GetBytes(packetLength); - Array.Reverse(lengthBytes); // Network byte order - packetBytes[2] = lengthBytes[0]; - packetBytes[3] = lengthBytes[1]; + packetBytes[2] = lengthBytes[1]; + packetBytes[3] = lengthBytes[0]; var packetBytesArray = packetBytes.ToArray(); @@ -82,7 +76,7 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) case PacketCode.AccountingRequest: case PacketCode.DisconnectRequest: case PacketCode.CoaRequest: - if (messageAuthenticatorPosition != -1) + if (messageAuthenticatorPosition != 0) { FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret); } @@ -97,7 +91,7 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) packetBytesArray) : packet.Authenticator.Value.ToArray(); - if (messageAuthenticatorPosition != -1) + if (messageAuthenticatorPosition != 0) { FillMessageAuthenticator( packetBytesArray, @@ -108,19 +102,12 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) break; default: - if (packet.RequestAuthenticator != null) - { - authenticator = _cryptoProvider.CalculateResponseAuthenticator( - sharedSecret, - packet.RequestAuthenticator.Value.ToArray(), - packetBytesArray); - } - else - { - authenticator = packet.Authenticator.Value.ToArray(); + if (packet.RequestAuthenticator == null) + { + Buffer.BlockCopy(packet.Authenticator.Value, 0, packetBytesArray, 4, 16); } - if (messageAuthenticatorPosition != -1) + if (messageAuthenticatorPosition != 0) { FillMessageAuthenticator( packetBytesArray, @@ -128,10 +115,18 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) sharedSecret, packet.RequestAuthenticator); } + + if (packet.RequestAuthenticator != null) + { + authenticator = _cryptoProvider.CalculateResponseAuthenticator( + sharedSecret, + packet.RequestAuthenticator.Value.ToArray(), + packetBytesArray); + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + } break; } - Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); return packetBytesArray; } @@ -146,6 +141,86 @@ public RadiusPacket CreateResponse(RadiusPacket request, PacketCode responseCode 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, @@ -153,13 +228,9 @@ private void FillMessageAuthenticator( SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null) { - // Zero out the Message-Authenticator field - for (int i = 0; i < 16; i++) - { - packetBytes[position + 2 + i] = 0; - } - // Calculate and insert Message-Authenticator + var temp = new byte[16]; + Buffer.BlockCopy(temp, 0, packetBytes, position + 2, 16); var messageAuthenticator = _cryptoProvider.CalculateMessageAuthenticator( sharedSecret, packetBytes, diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs index d8ea11df..9b4a1d35 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Client/RadiusClient.cs @@ -74,9 +74,7 @@ public RadiusClient(IPEndPoint localEndpoint, ILogger logger) { if (_pendingRequests.TryAdd(key, pendingRequest)) { - await _udpClient.SendAsync(requestPacket, remoteEndpoint); - - // Ожидаем завершения задачи (ответ или таймаут) + await _udpClient.SendAsync(requestPacket, remoteEndpoint, timeoutCancellation.Token); return await pendingRequest.TaskCompletionSource.Task; } else diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs index b6b070c5..32fd01a0 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Crypto/RadiusCryptoProvider.cs @@ -44,13 +44,15 @@ public bool ValidateMessageAuthenticator( var tempPacket = new byte[packet.Length]; packet.CopyTo(tempPacket, 0); - for (int i = 0; i < 16; i++) - { - tempPacket[position + 2 + i] = 0; - } - var calculated = CalculateMessageAuthenticator(secret, tempPacket, requestAuth); - return calculated.SequenceEqual(messageAuth); - // return CryptographicOperations.FixedTimeEquals(calculated, messageAuth); + // 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 byte[] DecryptPassword(SharedSecret secret, RadiusAuthenticator authenticator, byte[] encryptedPassword) @@ -60,16 +62,10 @@ public byte[] DecryptPassword(SharedSecret secret, RadiusAuthenticator authentic private static byte[] CalculateAuthenticator(SharedSecret secret, byte[] packet, byte[] requestAuth) { - var buffer = new byte[packet.Length + secret.Bytes.Length]; - packet.CopyTo(buffer, 0); - secret.Bytes.CopyTo(buffer.AsMemory(packet.Length)); - - if (requestAuth.Length == 16) - { - requestAuth.CopyTo(buffer, 4); - } + var responseAuthenticator = packet.Concat(secret.Bytes).ToArray(); + Buffer.BlockCopy(requestAuth, 0, responseAuthenticator, 4, 16); using var md5 = MD5.Create(); - return md5.ComputeHash(buffer); + return md5.ComputeHash(responseAuthenticator); } } \ 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 index 543a703a..2b04c994 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/IRadiusAttributeParser.cs @@ -4,5 +4,6 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; public interface IRadiusAttributeParser { - ParsedAttribute? Parse(byte[] attributeData, RadiusAuthenticator authenticator, SharedSecret sharedSecret); + 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/RadiusAttributeParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs index cf5a132e..d0ac4daf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Crypto; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; @@ -12,6 +13,9 @@ public class RadiusAttributeParser : IRadiusAttributeParser private readonly IRadiusDictionary _radiusDictionary; private readonly IRadiusCryptoProvider _cryptoProvider; private readonly ILogger _logger; + const int VendorSpecific = 26; + const int MessageAuthenticator = 80; + const int UserPassword = 2; public RadiusAttributeParser( IRadiusDictionary radiusDictionary, @@ -23,33 +27,16 @@ public RadiusAttributeParser( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public ParsedAttribute? Parse(byte[] attributeData, RadiusAuthenticator authenticator, SharedSecret sharedSecret) + public ParsedAttribute? Parse(byte[] attributeData, byte typeCode, RadiusAuthenticator authenticator, SharedSecret sharedSecret) { - if (attributeData == null || attributeData.Length < 2) - return null; - - byte typeCode = attributeData[0]; - byte length = attributeData[1]; - - if (length > attributeData.Length) - return null; - - byte[] contentBytes = new byte[length - 2]; - if (contentBytes.Length > 0) - { - Buffer.BlockCopy(attributeData, 2, contentBytes, 0, contentBytes.Length); - } - try { - if (typeCode == 26) // Vendor-Specific - { - return ParseVendorSpecificAttribute(contentBytes, authenticator, sharedSecret); - } - else + if (typeCode == VendorSpecific) // Vendor-Specific { - return ParseStandardAttribute(typeCode, contentBytes, authenticator, sharedSecret); + return ParseVendorSpecificAttribute(attributeData, authenticator, sharedSecret); } + return ParseStandardAttribute(typeCode, attributeData, authenticator, sharedSecret); + } catch (Exception ex) { @@ -123,89 +110,48 @@ public RadiusAttributeParser( if (content == null) return null; - bool isMessageAuthenticator = attributeDefinition.Code == 80; // Message-Authenticator + bool isMessageAuthenticator = attributeDefinition.Code == MessageAuthenticator; return new ParsedAttribute(attributeDefinition.Name, content, isMessageAuthenticator); } - private object ParseContentBytes( + private static object? ParseContentBytes( byte[] contentBytes, string type, uint code, RadiusAuthenticator authenticator, SharedSecret sharedSecret) { - switch (type.ToLowerInvariant()) + switch (type) { - case "string": - case "tagged-string": - return ParseString(contentBytes); - - case "octets": - if (code == 2) // User-Password + 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 _cryptoProvider.DecryptPassword(sharedSecret, authenticator, contentBytes); + return Encoding.UTF8.GetString(contentBytes); } + return contentBytes; - - case "integer": - case "tagged-integer": - return ParseInteger(contentBytes); - - case "ipaddr": - return ParseIpAddress(contentBytes); - - case "date": - return ParseDate(contentBytes); - - case "ifid": - return contentBytes; - - default: - _logger.LogWarning("Unknown attribute type: {Type}", type); + + case DictionaryAttribute.TypeOctet: + // If this is a password attribute it must be decrypted + if (code == UserPassword) + { + return RadiusPasswordProtector.Decrypt(sharedSecret, authenticator, contentBytes); + } + return contentBytes; - } - } - private static string ParseString(byte[] bytes) - { - // Try to decode as UTF-8, fall back to ASCII if invalid - try - { - return Encoding.UTF8.GetString(bytes).TrimEnd('\0'); - } - catch - { - return Encoding.ASCII.GetString(bytes).TrimEnd('\0'); - } - } + case DictionaryAttribute.TypeInteger: + case DictionaryAttribute.TypeTaggedInteger: + return BitConverter.ToInt32(contentBytes.Reverse().ToArray(), 0); + + case DictionaryAttribute.TypeIpAddr: + return new IPAddress(contentBytes); - private static int ParseInteger(byte[] bytes) - { - switch (bytes.Length) - { - case 4: - Array.Reverse(bytes); - return BitConverter.ToInt32(bytes, 0); - case 2: - Array.Reverse(bytes); - return BitConverter.ToInt16(bytes, 0); - case 1: - return bytes[0]; default: - throw new InvalidOperationException($"Invalid integer length: {bytes.Length}"); + return null; } } - - private static IPAddress ParseIpAddress(byte[] bytes) - { - return new IPAddress(bytes); - } - - private static DateTime ParseDate(byte[] bytes) - { - Array.Reverse(bytes); - uint seconds = BitConverter.ToUInt32(bytes, 0); - return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds); - } } \ 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 index e0323eaa..7857b93a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; @@ -11,6 +12,10 @@ public class RadiusPacketParser : IRadiusPacketParser 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, @@ -39,20 +44,35 @@ private RadiusPacket ParseInternal( ValidatePacketLength(packetBytes); ValidatePacketLengthField(packetBytes); - var code = (PacketCode)packetBytes[0]; - var identifier = packetBytes[1]; - var authenticatorBytes = new byte[16]; - Buffer.BlockCopy(packetBytes, 4, authenticatorBytes, 0, 16); - var authenticator = new RadiusAuthenticator(authenticatorBytes); - - var header = new RadiusPacketHeader(code, identifier, authenticator); + 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) @@ -83,52 +103,42 @@ private void ParseAttributes( RadiusPacket packet, SharedSecret sharedSecret) { - int position = 20; - int messageAuthenticatorPosition = -1; - byte[] messageAuthenticator = null; + int position = AttributesFieldPosition; + int messageAuthenticatorPosition = 0; + ushort packetLength = GetPacketLength(packetBytes); + while (position < packetBytes.Length) { - if (position + 1 >= packetBytes.Length) - { - throw new InvalidOperationException("Invalid attribute: incomplete header"); - } - - byte typeCode = packetBytes[position]; - byte length = packetBytes[position + 1]; - - if (length < 2) - { - throw new InvalidOperationException($"Invalid attribute length: {length}"); - } + var typeCode = packetBytes[position]; + var length = packetBytes[position + 1]; - if (position + length > packetBytes.Length) + if (position + length > packetLength) { - throw new InvalidOperationException( - $"Attribute exceeds packet boundary. Position: {position}, Length: {length}, Packet Length: {packetBytes.Length}"); + throw new ArgumentOutOfRangeException(); } - var attributeData = new byte[length]; - Buffer.BlockCopy(packetBytes, position, attributeData, 0, length); + var attributeData = new byte[length - 2]; + Buffer.BlockCopy(packetBytes, position + 2, attributeData, 0, length - 2); try { - var parsedAttribute = _attributeParser.Parse(attributeData, packet.Authenticator, sharedSecret); + + var parsedAttribute = _attributeParser.Parse(attributeData, typeCode, packet.Authenticator, sharedSecret); if (parsedAttribute != null) { packet.AddAttributeValue(parsedAttribute.Name, parsedAttribute.Value); - if (parsedAttribute.IsMessageAuthenticator) { messageAuthenticatorPosition = position; - if (parsedAttribute.Value is byte[] authBytes) - { - messageAuthenticator = authBytes; - } } } } + 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}", @@ -138,8 +148,9 @@ private void ParseAttributes( position += length; } - if (messageAuthenticatorPosition != -1 && messageAuthenticator != null) + if (messageAuthenticatorPosition != 0) { + var messageAuthenticator = packet.GetAttribute("Message-Authenticator"); var isValid = _cryptoProvider.ValidateMessageAuthenticator( packetBytes, messageAuthenticator, diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs index c758e436..b3ae8076 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -36,57 +36,39 @@ 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 (ShouldProxyResponse(request)) + + 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) { - await ProxyResponseAsync(request); + //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); + + Console.WriteLine(responsePacket.Authenticator.Value.ToString()); await SendResponsePacketAsync(responsePacket, request); LogResponseSent(responsePacket, request); } - private static bool ShouldProxyResponse(SendAdapterResponseRequest request) - { - // EAP challenge - if (request.ResponsePacket?.IsEapMessageChallenge == true) - return true; - - // Vendor ACL request - if (request.RequestPacket.IsVendorAclRequest && request.ResponsePacket != null) - return true; - - return false; - } - - private async Task ProxyResponseAsync(SendAdapterResponseRequest request) - { - if (request.ResponsePacket == null) - return; - - var logMessage = request.RequestPacket.IsVendorAclRequest - ? "Proxying #ACSACL#" - : "Proxying EAP-Message Challenge"; - - _logger.LogDebug( - "{Action} to {Host}:{Port} id={Id}", - logMessage, - request.RemoteEndpoint.Address, - request.RemoteEndpoint.Port, - request.RequestPacket.Identifier); - - await SendResponsePacketAsync(request.ResponsePacket, request); - } - private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) { var responsePacketCode = DetermineResponseCode(request.FirstFactorStatus, request.SecondFactorStatus); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs index b9a63a1b..0f577626 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -25,13 +25,14 @@ public RadiusReplyAttributeService( 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.Any()) + if (values.Count != 0) { result[attribute.Key] = values; } @@ -133,7 +134,7 @@ private static bool MatchesUserNameCondition(IReadOnlyList conditions, s return false; } - private static bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet userGroups) + private static bool MatchesUserGroupCondition(IReadOnlyList conditions, HashSet? userGroups) { if (userGroups == null || userGroups.Count == 0) return false; diff --git a/src/Multifactor.Radius.Adapter.v2/App.config b/src/Multifactor.Radius.Adapter.v2/App.config index 157a1792..a8434d70 100644 --- a/src/Multifactor.Radius.Adapter.v2/App.config +++ b/src/Multifactor.Radius.Adapter.v2/App.config @@ -43,5 +43,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Dockerfile b/src/Multifactor.Radius.Adapter.v2/Dockerfile index 445130f1..b53d4248 100644 --- a/src/Multifactor.Radius.Adapter.v2/Dockerfile +++ b/src/Multifactor.Radius.Adapter.v2/Dockerfile @@ -1,23 +1,36 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base USER app WORKDIR /app -EXPOSE 8080 +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 +USER app WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +# Устанавливаем OpenLDAP и создаем симлинк +RUN apt-get update && \ + apt-get install -y libldap-2.5-0 && \ + ln -s /usr/lib/x86_64-linux-gnu/libldap-2.5.so.0 /usr/lib/x86_64-linux-gnu/libldap-2.4.so.2 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Копируем опубликованное приложение из стадии publish COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "RediusV2.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "RadiusV2.dll"] \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs index 12ff336d..d879fc9e 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -18,7 +18,7 @@ public class AdapterServer : IAsyncDisposable private readonly SemaphoreSlim _concurrencyLimiter; private readonly ConcurrentBag _activeProcessingTasks = []; - //Возможно сразу в конфигурацию вынести + //TODO Возможно сразу в конфигурацию вынести private const int ShoutDownTimeout = 30; private const int MaxConcurrentRequests = 1000; @@ -180,9 +180,9 @@ public async ValueTask DisposeAsync() { await StopAsync(); - _concurrencyLimiter?.Dispose(); + _concurrencyLimiter.Dispose(); _cts?.Dispose(); - _udpClient?.Dispose(); + _udpClient.Dispose(); GC.SuppressFinalize(this); } diff --git a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs index e0a4e3e6..0a1d696e 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs @@ -9,6 +9,7 @@ public class ServerHost : IHostedService private readonly ILogger _logger; private Task? _serverTask; private CancellationTokenSource? _cts; + private const int ShoutDownTimeout = 30; public ServerHost(AdapterServer server, ILogger logger) { @@ -44,7 +45,7 @@ public async Task StopAsync(CancellationToken cancellationToken) if (_serverTask is { IsCompleted: false }) { await Task.WhenAny(_serverTask, - Task.Delay(TimeSpan.FromSeconds(30), cancellationToken)); + Task.Delay(TimeSpan.FromSeconds(ShoutDownTimeout), cancellationToken)); } _logger.LogInformation("RADIUS server host stopped"); From 689237d66875b03d21b9e01e4a6230296e1d1b17 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Fri, 30 Jan 2026 09:34:55 +0300 Subject: [PATCH 05/11] another fix --- .../Features/Pipeline/RadiusPipeline.cs | 2 +- .../Features/Pipeline/Steps/UserNameValidationStep.cs | 4 ++-- .../Multifactor.Radius.Adapter.v2.Application.csproj | 2 +- .../Multifactor.Radius.Adapter.v2.Infrastructure.csproj | 2 +- .../Multifactor.Radius.Adapter.v2.csproj | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs index c7d659d1..4b946e1c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipeline.cs @@ -15,7 +15,7 @@ public RadiusPipeline(List steps) public async Task ExecuteAsync(RadiusPipelineContext context) { - + ArgumentNullException.ThrowIfNull(context); foreach (var step in _steps) { await step.ExecuteAsync(context); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs index 18978814..5315afe7 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserNameValidationStep.cs @@ -57,10 +57,10 @@ private static bool IsPermittedSuffix(string domain, IReadOnlyList inclu { if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain)); - if (includedSuffixes.Count > 0) + if (includedSuffixes != null && includedSuffixes.Count > 0) return includedSuffixes.Any(included => included.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); - if (excludedSuffixes.Count > 0) + if (excludedSuffixes != null && excludedSuffixes.Count > 0) return excludedSuffixes.All(excluded => !excluded.Equals(domain, StringComparison.CurrentCultureIgnoreCase)); return true; 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 index c295aadf..016ba518 100644 --- 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 @@ -7,7 +7,7 @@ - + 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 index a55338a5..e8f7cc65 100644 --- 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 @@ -8,7 +8,7 @@ - + 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 ab96dce5..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,7 +23,7 @@ - + From 3fa2cb70aef1c9f1d6dac90093bb24a61a20c2cc Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 5 Feb 2026 03:28:56 +0300 Subject: [PATCH 06/11] end refactoring --- .../ChallengeProcessorProviderTests.cs | 1 - .../ChangePasswordChallengeProcessorTests.cs | 2 - .../Fixtures/TestHostFactory.cs | 2 +- .../LdapIdentityTests.cs | 3 +- .../LdapUserTests.cs | 1 - .../PreAuthModeSettingsTests.cs | 2 +- .../RadiusReplyAttributeRewriterTests.cs | 4 +- .../Cache/ICacheService.cs | 2 +- .../Models/ClientConfiguration.cs | 54 - .../Models/IClientConfiguration.cs | 34 + ...uration.cs => ILdapServerConfiguration.cs} | 23 +- ...yAttribute.cs => IRadiusReplyAttribute.cs} | 14 +- .../Models/IRootConfiguration.cs | 25 + .../Configuration/Models/RootConfiguration.cs | 41 - .../Models/ServiceConfiguration.cs | 19 +- .../Features/Ldap/Models/LdapProfile.cs | 7 +- .../Models/SecondFactorResponse.cs | 2 +- .../Multifactor/RequestDataExtractor.cs | 1 - .../ChangePasswordChallengeProcessor.cs | 3 +- .../SecondFactorChallengeProcessor.cs | 1 - .../BindNameFormat/FreeIpaFormatter.cs | 3 +- .../FirstFactor/LdapFirstFactorProcessor.cs | 1 - .../Pipeline/Interfaces/IPipelineProvider.cs | 2 +- .../Interfaces/IRadiusPipelineFactory.cs | 2 +- .../Pipeline/Models/RadiusPipelineContext.cs | 8 +- .../Features/Pipeline/Models/UserIdentity.cs | 3 +- .../Pipeline/Models/UserPassphrase.cs | 4 +- .../Pipeline/RadiusPipelineFactory.cs | 12 +- .../Pipeline/RadiusPipelineProvider.cs | 2 +- .../Steps/AccessGroupsCheckingStep.cs | 1 - .../Pipeline/Steps/LdapSchemaLoadingStep.cs | 1 - .../Pipeline/Steps/ProfileLoadingStep.cs | 3 +- .../Pipeline/Steps/SecondFactorStep.cs | 1 - .../Pipeline/Steps/UserGroupLoadingStep.cs | 1 - .../Exceptions/PipelineNotFoundException.cs | 6 - .../Models/GetReplyAttributesRequest.cs | 4 +- .../Features/Radius/Models/RadiusPacket.cs | 1 - .../Radius/Models/RadiusPacketHeader.cs | 2 +- .../Models/SendAdapterResponseRequest.cs | 2 +- .../Radius/Services/IRadiusPacketProcessor.cs | 2 +- .../Radius/Services/RadiusPacketProcessor.cs | 32 +- .../Constants/RadiusAdapterConstants.cs | 18 - .../E2EClientConfigurationsProvider.cs | 23 - .../E2ETestBase.cs | 178 --- .../E2ETestsUtils.cs | 82 - .../TestClientConfigsProvider.cs | 64 - .../TestConfigProviderOptions.cs | 8 - .../ConfigLoading/TestRootConfigProvider.cs | 29 - .../Fixtures/Models/ConfigSensitiveData.cs | 34 - .../Fixtures/Models/E2ERadiusConfiguration.cs | 9 - .../Models/RadiusConfigurationModel.cs | 14 - .../Fixtures/RadiusPacketFactory.cs | 34 - .../Fixtures/ServiceCollectionExtensions.cs | 78 - .../Fixtures/TestEnvironment.cs | 39 - ...tor.Radius.Adapter.v2.EndToEndTests.csproj | 35 - .../RadiusFixtures.cs | 25 - .../Tests/AccessChallengeTests.cs | 203 --- .../Tests/AccessRequestAttributesTests.cs | 153 -- .../Tests/BypassWhenApiUnreachableTests.cs | 238 --- .../Tests/ChangePasswordTests.cs | 204 --- .../Tests/FirstFactorTests.cs | 232 --- .../MultipleActiveDirectory2FaGroupsTests.cs | 158 -- .../MultipleActiveDirectoryGroupsTests.cs | 157 -- .../Tests/PreSecondFactorTests.cs | 469 ------ .../Tests/ReplyAttributesTests.cs | 101 -- ...ingleActiveDirectory2FaBypassGroupTests.cs | 199 --- .../SingleActiveDirectory2FaGroupTests.cs | 199 --- .../Tests/SingleActiveDirectoryGroupTests.cs | 237 --- .../Udp/UdpData.cs | 25 - .../Udp/UdpSocket.cs | 44 - .../Ldap/CustomLdapConnectionFactory.cs | 1 - .../Adapters/Ldap/LdapAdapter.cs | 1 - .../Http/RoundRobinEndpointSelector.cs | 1 - .../Multifactor/Models/AccessRequestDto.cs | 1 + .../Adapters/Multifactor/MultifactorApi.cs | 1 - .../PacketHandler/RadiusUdpAdapter.cs | 5 +- .../Adapters/Udp/CustomUdpClient.cs | 2 +- .../InvalidConfigurationException.cs | 69 +- .../Loader/ConfigurationLoader.cs | 191 ++- .../Loader/IConfigurationLoader.cs | 3 +- .../Models/ClientConfiguration.cs | 82 + .../Models/ConfigurationFile.cs | 156 ++ .../Attributes/DictionaryAttribute.cs | 2 +- .../Attributes/DictionaryVendorAttribute.cs | 2 +- .../Attributes/VendorSpecificAttribute.cs | 2 +- .../Dictionary/IRadiusDictionary.cs | 4 +- .../Dictionary/RadiusDictionary.cs | 6 +- .../Models/LdapServerConfiguration.cs | 101 ++ .../Models/RadiusReplyAttribute.cs | 14 + .../Models/RootConfiguration.cs | 62 + .../Parser/ConfigurationValueProcessor.cs | 298 ++++ .../Parser/IConfigurationParser.cs | 9 - .../Configurations/Parser/ValueParser.cs | 247 --- .../Parser/XmlConfigurationParser.cs | 223 --- .../Reader/ConfigurationBuilderExtensions.cs | 20 + .../Reader/ConfigurationReader.cs | 30 + .../Reader/EnvironmentReader.cs | 14 - ...vironmentVariablesConfigurationProvider.cs | 39 + ...EnvironmentVariablesConfigurationSource.cs | 16 + .../Reader/XmlConfigurationProvider.cs | 213 +++ .../Reader/XmlConfigurationSource.cs | 13 + .../Configurations/Reader/XmlReader.cs | 58 - .../Extensions/InfrastructureExtensions.cs | 21 +- .../Logging/SerilogLoggerFactory.cs | 21 +- .../Builders/RadiusAttributeSerializer.cs | 4 +- .../Radius/Builders/RadiusPacketBuilder.cs | 45 +- .../Radius/Parsers/RadiusAttributeParser.cs | 7 +- .../Radius/Parsers/RadiusPacketParser.cs | 1 - .../Radius/Sender/AdapterResponseSender.cs | 3 - .../Services/RadiusAttributeTypeConverter.cs | 2 +- .../Radius/Services/RadiusPacketService.cs | 4 +- .../Services/RadiusReplyAttributeService.cs | 16 +- .../Attributes/ConfigAttribute.cs | 33 - .../ChallengeProcessorProviderTests.cs | 62 - .../ChangePasswordChallengeProcessorTests.cs | 366 ----- .../SecondFactorChallengeProcessorTests.cs | 294 ---- .../Multifactor/MultifactorApiServiceTests.cs | 627 ++++++++ .../ChallengeProcessorProviderTests.cs | 103 ++ .../ChangePasswordChallengeProcessorTests.cs | 352 +++++ .../SecondFactorChallengeProcessorTests.cs | 548 +++++++ .../ActiveDirectoryFormatterTests.cs | 65 + .../BindNameFormat/FreeIpaFormatterTests.cs | 140 ++ .../LdapBindNameFormatterProviderTests.cs | 63 + .../MultiDirectoryFormatterTests.cs | 126 ++ .../BindNameFormat/OpenLdapFormatterTests.cs | 126 ++ .../BindNameFormat/SambaFormatterTests.cs | 126 ++ .../FirstFactorProcessorProviderTests.cs | 72 + .../LdapFirstFactorProcessorTests.cs | 253 +++ .../NoneFirstFactorProcessorTests.cs | 92 ++ .../RadiusFirstFactorProcessorTests.cs | 257 ++++ .../Pipeline/RadiusPipelineFactoryTests.cs | 203 +++ .../Pipeline/RadiusPipelineProviderTests.cs | 114 ++ .../Pipeline/RadiusPipelineTests.cs | 143 ++ .../Steps/AccessChallengeStepTests.cs | 267 ++++ .../Steps/AccessGroupsCheckingStepTests.cs | 222 +++ .../Steps/AccessRequestFilteringStepTests.cs | 126 ++ .../Pipeline/Steps/FirstFactorStepTests.cs | 302 ++++ .../Pipeline/Steps/IpWhiteListStepTests.cs | 152 ++ .../Steps/LdapSchemaLoadingStepTests.cs | 286 ++++ .../Pipeline/Steps/PreAuthCheckStepTests.cs | 168 ++ .../Pipeline/Steps/PreAuthPostCheckTests.cs | 188 +++ .../Pipeline/Steps/ProfileLoadingStepTests.cs | 255 ++++ .../Pipeline/Steps/SecondFactorStepTests.cs | 327 ++++ .../Steps/StatusServerFilteringStepTests.cs | 199 +++ .../Steps/UserGroupLoadingStepTests.cs | 270 ++++ .../Steps/UserNameValidationStepTests.cs | 190 +++ .../Radius/RadiusPacketProcessorTests.cs | 371 +++++ .../Security/ProtectionServiceTests.cs | 200 +++ .../Security/RadiusPasswordProtectorTests.cs | 179 +++ .../AppSettingsTests.cs | 939 ------------ .../LdapSettingsTests.cs | 867 ----------- .../LdapServerConfigurationTests.cs | 257 ---- .../LoadXmlConfigurationTests.cs | 154 -- .../RadiusAdapterConfigurationFactoryTests.cs | 129 -- .../RadiusConfigurationFileTests.cs | 66 - .../ServiceConfigurationFactoryTests.cs | 232 --- .../ServiceConfigurationTests.cs | 98 -- .../CustomLdapSchemaLoaderTests.cs | 38 - .../LdapFirstFactorProcessorTests.cs | 128 -- .../RadiusFirstFactorProcessorTests.cs | 115 -- .../Fixture/ConfigUtils.cs | 22 - .../Fixture/EmptyStringsListInput.cs | 19 - .../Fixture/PacketExamples.cs | 80 - .../Fixture/TestUtils.cs | 23 - .../LdapForest/LdapForestLoaderTests.cs | 0 .../LdapPasswordChangerTests.cs | 99 -- .../LdapProfile/LdapProfileLoaderTests.cs | 42 - .../LdapProfile/LdapProfileServiceTests.cs | 143 -- .../LdapProfile/LdapProfileTest.cs | 153 -- ...Multifactor.Radius.Adapter.v2.Tests.csproj | 9 +- .../NetBiosServiceTests.cs | 0 .../PipelineTests/BuildPipelineTests.cs | 28 - .../PipelineTests/PerformanceTests.cs | 69 - .../PipelineConfigurationFactoryTests.cs | 209 --- .../PipelineTests/PipelineExecutionTests.cs | 61 - .../AccessGroupsCheckingStepTests.cs | 186 --- .../AccessRequestFilteringStepTests.cs | 53 - .../StepsTests/PreAuthCheckStepTests.cs | 58 - .../StepsTests/PreAuthPostCheckStepTests.cs | 45 - .../StepsTests/ProfileLoadingStepTests.cs | 166 -- .../StepsTests/SecondFactorStepTests.cs | 451 ------ .../StatusServerFilteringStepTests.cs | 54 - .../StepsTests/UserGroupLoadingStepTests.cs | 276 ---- .../Radius/NasIdentifierParserTests.cs | 36 - .../PacketService/AttributeReadingTests.cs | 142 -- .../PacketService/RadiusPacketParsingTests.cs | 79 - .../PacketService/ResponsePacketTests.cs | 69 - .../Radius/RadiusAttributeTests.cs | 75 - .../Radius/RadiusPacketTests.cs | 127 -- .../Server/UdpPacketHandlerTests.cs | 72 - .../AddPipelineTests.cs | 67 - .../TestEnvironment.cs | 37 - .../TestEnvironmentVariables.cs | 44 - .../Unit/AdapterResponseSenderTests.cs | 421 ----- .../LdapFirstFactorProcessorTests.cs | 119 -- .../RadiusFirstFactorProcessorTests.cs | 231 --- .../ActiveDirectoryTests.cs | 24 - .../FreeIpaTests.cs | 73 - .../LdapBindNameFormatterProviderTests.cs | 49 - .../MultiDirectoryTests.cs | 73 - .../OpenLdapTests.cs | 73 - .../LdapBindNameFormatterTests/SambaTests.cs | 73 - .../Unit/LdapGroupServiceTests.cs | 292 ---- .../MultifactorApiServiceTests.cs | 1360 ----------------- .../MultifactorApi/MultifactorApiTests.cs | 130 -- .../PipelineSteps/IpWhiteListStepTests.cs | 114 -- .../StepsTests/UserNameValidationStepTests.cs | 182 --- .../Unit/RadiusAttributeTypeConverterTests.cs | 69 - .../Unit/RadiusReplyAttributeServiceTests.cs | 262 ---- .../UserIdentityTests/UserIdentityTests.cs | 40 - src/Multifactor.Radius.Adapter.v2/App.config | 16 +- src/Multifactor.Radius.Adapter.v2/Dockerfile | 22 +- .../Server/AdapterServer.cs | 2 +- .../Server/ServerHost.cs | 4 +- src/multifactor-radius-adapter.sln | 7 +- 215 files changed, 8609 insertions(+), 14544 deletions(-) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs rename src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/{LdapServerConfiguration.cs => ILdapServerConfiguration.cs} (58%) rename src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/{RadiusReplyAttribute.cs => IRadiusReplyAttribute.cs} (62%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Constants/RadiusAdapterConstants.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestConfigProviderOptions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/ConfigSensitiveData.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ServiceCollectionExtensions.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/TestEnvironment.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Multifactor.Radius.Adapter.v2.EndToEndTests.csproj delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpData.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.EndToEndTests/Udp/UdpSocket.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/{ => Models}/Dictionary/Attributes/DictionaryAttribute.cs (98%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/{ => Models}/Dictionary/Attributes/DictionaryVendorAttribute.cs (98%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/{ => Models}/Dictionary/Attributes/VendorSpecificAttribute.cs (98%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/{ => Models}/Dictionary/IRadiusDictionary.cs (94%) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/{ => Models}/Dictionary/RadiusDictionary.cs (96%) create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RadiusReplyAttribute.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/PrefixEnvironmentVariablesConfigurationSource.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlConfigurationSource.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Multifactor/MultifactorApiServiceTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChallengeProcessorProviderTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/ChangePasswordChallengeProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/AccessChallenge/SecondFactorChallengeProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/ActiveDirectoryFormatterTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/FreeIpaFormatterTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/LdapBindNameFormatterProviderTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/MultiDirectoryFormatterTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/OpenLdapFormatterTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/BindNameFormat/SambaFormatterTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/FirstFactorProcessorProviderTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineFactoryTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessGroupsCheckingStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthCheckStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/StatusServerFilteringStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserGroupLoadingStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/UserNameValidationStepTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/ProtectionServiceTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Application/Security/RadiusPasswordProtectorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/LdapSettingsTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LoadXmlConfigurationTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusAdapterConfigurationFactoryTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/RadiusConfigurationFileTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Fixture/ConfigUtils.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Fixture/EmptyStringsListInput.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Fixture/PacketExamples.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/LdapForest/LdapForestLoaderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileTest.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/NetBiosServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironment.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/TestEnvironmentVariables.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/ActiveDirectoryTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/FreeIpaTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/LdapBindNameFormatterProviderTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/MultiDirectoryTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/OpenLdapTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapBindNameFormatterTests/SambaTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusAttributeTypeConverterTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs diff --git a/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs b/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs index 1e28a12a..825c3cb5 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs b/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs index e63ec2c4..f68c991a 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs @@ -1,10 +1,8 @@ using System.Net; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; -using MultiFactor.Radius.Adapter.Core; using MultiFactor.Radius.Adapter.Core.Framework.Context; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ClientLevel; diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs index 615f3e06..4a8b2094 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs @@ -8,9 +8,9 @@ using MultiFactor.Radius.Adapter.Extensions; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; -using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Core; using MultiFactor.Radius.Adapter.Tests.Fixtures.ConfigLoading; using System.Net; +using MultiFactor.Radius.Adapter.Server; namespace MultiFactor.Radius.Adapter.Tests.Fixtures; diff --git a/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs b/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs index 614ed849..1c7cd524 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs @@ -1,5 +1,4 @@ -using MultiFactor.Radius.Adapter.Core.Services.Ldap; -using MultiFactor.Radius.Adapter.Services.Ldap; +using MultiFactor.Radius.Adapter.Services.Ldap; namespace MultiFactor.Radius.Adapter.Tests { diff --git a/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs b/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs index 70da6c8c..19cd6dbd 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using MultiFactor.Radius.Adapter.Core.Services.Ldap; using MultiFactor.Radius.Adapter.Services.Ldap; namespace MultiFactor.Radius.Adapter.Tests diff --git a/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs b/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs index 44bd5233..9d526bb1 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs @@ -1,6 +1,6 @@ using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Features.PreAuthModeFeature; -namespace MultiFactor.Radius.Adapter.Tests.AdapterConfig; +namespace MultiFactor.Radius.Adapter.Tests; public partial class ConfigurationLoadingTests { diff --git a/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs b/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs index 723ddc31..f86777e8 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs @@ -1,8 +1,6 @@ -using System.Net; -using FluentAssertions; +using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Moq; -using MultiFactor.Radius.Adapter.Core.Framework.Context; using MultiFactor.Radius.Adapter.Core.Radius.Attributes; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ClientLevel; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs index e9a18c1e..85ed7dec 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Cache/ICacheService.cs @@ -2,7 +2,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Cache; public interface ICacheService { - //TODO check where use. + //TODO разделить на несколько void Set(string key, T value, DateTimeOffset expirationDate); bool TryGetValue(string key, out T? value); void Remove(string key); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs deleted file mode 100644 index 2d3136b6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ClientConfiguration.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -using Multifactor.Radius.Adapter.v2.Shared.Attributes; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; - -public class ClientConfiguration -{ - public string Name { get; set; } - - [ConfigParameter("multifactor-nas-identifier")] - public string MultifactorNasIdentifier { get; set; } - [ConfigParameter("multifactor-shared-secret")] - public string MultifactorSharedSecret { get; set; } - [ConfigParameter("sign-up-group")] - public IReadOnlyList SignUpGroups { get; set; } = []; - [ConfigParameter("bypass-second-factor-when-api-unreachable",true)] - public bool BypassSecondFactorWhenApiUnreachable { get; set; } - [ConfigParameter("first-factor-authentication-source")] - public AuthenticationSource FirstFactorAuthenticationSource { get; set; } - [ConfigParameter("adapter-client-endpoint")] - public IPEndPoint AdapterClientEndpoint { get; set; } - - [ConfigParameter("radius-client-ip")] - public IPAddress? RadiusClientIp { get; set; } - [ConfigParameter("radius-client-nas-identifier")] - public string RadiusClientNasIdentifier { get; set; } - [ConfigParameter("radius-shared-secret")] - public string RadiusSharedSecret { get; set; } - [ConfigParameter("nps-server-endpoint")] - public IPEndPoint[] NpsServerEndpoints { get; set; } - [ConfigParameter("nps-server-timeout")] - public TimeSpan NpsServerTimeout { get; set; } //"00:00:05" - - [ConfigParameter("privacy-mode")] - public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } - - [ConfigParameter("pre-authentication-method")] - public PreAuthMode? PreAuthenticationMethod { get; set; } - [ConfigParameter("authentication-cache-lifetime")] - public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; - [ConfigParameter("invalid-credential-delay")] - public (int min, int max) InvalidCredentialDelay { get; set; } - [ConfigParameter("calling-station-id-attribute")] - public string CallingStationIdAttribute { get; set; } //TODO not used - [ConfigParameter("ip-white-list")] - public IReadOnlyList IpWhiteList { get; set; } - - [ComplexConfigParameter("ldapServers")] - public IReadOnlyList? LdapServers { get; set; } - [ComplexConfigParameter("RadiusReply")] - public IReadOnlyDictionary? ReplyAttributes { get; set; } -} \ No newline at end of file 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..60fe8e57 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs @@ -0,0 +1,34 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface 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 IPAddress? RadiusClientIp { get; set; } + public string RadiusClientNasIdentifier { get; set; } + public string RadiusSharedSecret { get; set; } + public IPEndPoint[] NpsServerEndpoints { get; set; } + public TimeSpan NpsServerTimeout { get; set; } + + public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } + + public PreAuthMode? PreAuthenticationMethod { get; set; } + public TimeSpan AuthenticationCacheLifetime { get; set; } + public (int min, int max)? 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; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs similarity index 58% rename from src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs index 63fe2115..b28c60c6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs @@ -1,48 +1,27 @@ using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Shared.Attributes; namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -public class LdapServerConfiguration +public interface ILdapServerConfiguration { - [ConfigParameter("connection-string")] public string ConnectionString { get; init; } - [ConfigParameter("username")] public string Username { get; init; } - [ConfigParameter("password")] public string Password { get; init; } - [ConfigParameter("bind-timeout-in-seconds")] public int BindTimeoutSeconds{ get; init; } - [ConfigParameter("access-groups")] public IReadOnlyList AccessGroups { get; init; } - [ConfigParameter("second-fa-groups")] public IReadOnlyList SecondFaGroups { get; init; } - [ConfigParameter("second-fa-bypass-groups")] public IReadOnlyList SecondFaBypassGroups { get; init; } - [ConfigParameter("load-nested-groups")] public bool LoadNestedGroups { get; init; } - [ConfigParameter("nested-groups-base-dn")] public IReadOnlyList NestedGroupsBaseDns { get; init; } - [ConfigParameter("authentication-cache-groups")] public IReadOnlyList AuthenticationCacheGroups { get; init; } - [ConfigParameter("phone-attributes")] public IReadOnlyList PhoneAttributes { get; init; } - [ConfigParameter("identity-attribute")] public string IdentityAttribute { get; init; } - [ConfigParameter("requires-upn")] public bool RequiresUpn { get; init; } - [ConfigParameter("enable-trusted-domains")] public bool TrustedDomainsEnabled { get; init; } - [ConfigParameter("enable-alternative-suffixes")] public bool AlternativeSuffixesEnabled { get; init; } - [ConfigParameter("included-domains")] public IReadOnlyList IncludedDomains { get; init; }//TODO not used - [ConfigParameter("excluded-domains")] public IReadOnlyList ExcludedDomains { get; init; }//TODO not used - [ConfigParameter("included-suffixes")] public IReadOnlyList IncludedSuffixes { get; init; } - [ConfigParameter("excluded-suffixes")] public IReadOnlyList ExcludedSuffixes { get; init; } - [ConfigParameter("bypass-second-factor-when-api-unreachable-groups")] public IReadOnlyList BypassSecondFactorWhenApiUnreachableGroups { get; init; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs similarity index 62% rename from src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs index 6252c6c6..a623369c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RadiusReplyAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs @@ -1,12 +1,12 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -public class RadiusReplyAttribute +public interface IRadiusReplyAttribute { - public string Name { get; init; } = string.Empty; - public object Value { get; init; } = string.Empty; - public IReadOnlyList UserGroupCondition { get; set; } = []; - public IReadOnlyList UserNameCondition { get; set; } = []; - public bool Sufficient { get; init; } + public string Name { get; set; } + public object Value { get; set; } + 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); -} +} \ 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..362401c8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs @@ -0,0 +1,25 @@ +using System.Net; + +namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; + +public interface IRootConfiguration +{ + + IReadOnlyList MultifactorApiUrls { get; set; } + string? MultifactorApiProxy { get; set; } + TimeSpan MultifactorApiTimeout { get; set; } + IPEndPoint? AdapterServerEndpoint { get; set; } + string LoggingLevel { get; set; } + string? LoggingFormat { get; set; } + bool SyslogUseTls { get; set; } + string? SyslogServer { get; set; } + string? SyslogFormat { get; set; } + string? SyslogFacility { get; set; } + string SyslogAppName { get; set; } + string? SyslogFramer { get; set; } + string? SyslogOutputTemplate { get; set; } + + string? ConsoleLogOutputTemplate { get; set; } + string? FileLogOutputTemplate { get; set; } + int LogFileMaxSizeBytes { get; set; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs deleted file mode 100644 index 90e5584e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/RootConfiguration.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Net; -using Multifactor.Radius.Adapter.v2.Shared.Attributes; - -namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; - -public class RootConfiguration -{ - [ConfigParameter("multifactor-api-url")] - public IReadOnlyList MultifactorApiUrls { get; set; } - [ConfigParameter("multifactor-api-proxy")] - public string? MultifactorApiProxy { get; set; } - [ConfigParameter("multifactor-api-timeout")] - public TimeSpan MultifactorApiTimeout { get; set; } - [ConfigParameter("adapter-server-endpoint")] - public IPEndPoint? AdapterServerEndpoint { get; set; } - [ConfigParameter("logging-level")] - public string LoggingLevel { get; set; } - [ConfigParameter("logging-format")] - public string LoggingFormat { get; set; } - [ConfigParameter("syslog-use-tls")] - public bool SyslogUseTls { get; set; } - [ConfigParameter("syslog-server")] - public string SyslogServer { get; set; } - [ConfigParameter("syslog-format")] - public string SyslogFormat { get; set; } - [ConfigParameter("syslog-facility")] - public string SyslogFacility { get; set; } - [ConfigParameter("syslog-app-name", "multifactor-radius")] - public string SyslogAppName { get; set; } - [ConfigParameter("syslog-framer")] - public string SyslogFramer { get; set; } - [ConfigParameter("syslog-output-template")] - public string SyslogOutputTemplate { get; set; } - - [ConfigParameter("console-log-output-template")] - public string ConsoleLogOutputTemplate { get; set; } - [ConfigParameter("file-log-output-template")] - public string FileLogOutputTemplate { get; set; } - [ConfigParameter("log-file-max-size-bytes", 1073741824)] - public int LogFileMaxSizeBytes { get; set; } -} \ 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 index ef5a14df..a0f121bd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -4,8 +4,19 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public class ServiceConfiguration { - public required RootConfiguration RootConfiguration { get; set; } - public required IReadOnlyList ClientsConfigurations { get; set; } - public ClientConfiguration? GetClientConfiguration(string nasIdentifier) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientNasIdentifier == nasIdentifier); - public ClientConfiguration? GetClientConfiguration(IPAddress ip) => ClientsConfigurations.FirstOrDefault(config => config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); + public required IRootConfiguration RootConfiguration { get; set; } + public required IReadOnlyList ClientsConfigurations { get; set; } + 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.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); + + } + public bool SingleClientMode { get; set; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs index 9161297e..db4d26e4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/LdapProfile.cs @@ -1,6 +1,5 @@ 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; @@ -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/Multifactor/Models/SecondFactorResponse.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs index a9926ef9..c48dbf95 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/Models/SecondFactorResponse.cs @@ -5,7 +5,7 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Multifactor.Models; public class SecondFactorResponse { public AuthenticationStatus Code { get; } public string? ReplyMessage { get; } - public string? State { get; } = null; + public string? State { get; } public SecondFactorResponse(AuthenticationStatus code, string? state = null, string? replyMessage = null) { Code = code; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs index b6d5da40..72274359 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs @@ -1,5 +1,4 @@ using System.Net; -using Multifactor.Core.Ldap.Attributes; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.AccessChallenge.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; 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 index 539b595e..4debbfc6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/ChangePasswordChallengeProcessor.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -32,7 +31,7 @@ public ChangePasswordChallengeProcessor( public ChallengeIdentifier AddChallengeContext(RadiusPipelineContext context) { ArgumentNullException.ThrowIfNull(context); - if (string.IsNullOrWhiteSpace(context.Passphrase.Password)) + if (string.IsNullOrWhiteSpace(context.Passphrase?.Password)) throw new InvalidOperationException("User password is required."); if (string.IsNullOrWhiteSpace(context.MustChangePasswordDomain)) 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 index 92ca11b8..bb6b4de5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/AccessChallenge/SecondFactorChallengeProcessor.cs @@ -1,7 +1,6 @@ using System.Text; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index b2547b34..61749df0 100644 --- 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 @@ -13,8 +13,7 @@ public string FormatName(string userName, ILdapProfile ldapProfile) { var identity = new UserIdentity(userName); - if (identity.Format == UserIdentityFormat.UserPrincipalName - || identity.Format == UserIdentityFormat.DistinguishedName) + if (identity.Format is UserIdentityFormat.UserPrincipalName or UserIdentityFormat.DistinguishedName) return userName; return ldapProfile.Dn.StringRepresentation; 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 index cd4c522c..3e9b50d2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/FirstFactor/LdapFirstFactorProcessor.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index 00723495..feb98bbe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IPipelineProvider.cs @@ -4,5 +4,5 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces public interface IPipelineProvider { - public IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration); + public IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration); } \ 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 index 4f1026fe..9af4918f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Interfaces/IRadiusPipelineFactory.cs @@ -4,5 +4,5 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Interfaces public interface IRadiusPipelineFactory { - IRadiusPipeline CreatePipeline(ClientConfiguration clientConfig); + IRadiusPipeline CreatePipeline(IClientConfiguration clientConfig); } \ No newline at end of file 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 index c8baa337..978c382d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/RadiusPipelineContext.cs @@ -10,8 +10,8 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; public class RadiusPipelineContext { public RadiusPacket RequestPacket { get; } - public ClientConfiguration ClientConfiguration { get; } - public LdapServerConfiguration? LdapConfiguration { get; } + public IClientConfiguration ClientConfiguration { get; } + public ILdapServerConfiguration? LdapConfiguration { get; } public UserPassphrase? Passphrase { get; set; } public ILdapSchema? LdapSchema { get; set; } public ILdapProfile? LdapProfile { get; set; } @@ -31,8 +31,8 @@ public class RadiusPipelineContext public RadiusPipelineContext( RadiusPacket requestPacket, - ClientConfiguration clientConfiguration, - LdapServerConfiguration? ldapServerConfig = null) + IClientConfiguration clientConfiguration, + ILdapServerConfiguration? ldapServerConfig = null) { RequestPacket = requestPacket; ClientConfiguration = clientConfiguration; 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 index a6fb4d1a..0d2a3ac3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserIdentity.cs @@ -1,4 +1,3 @@ -using Multifactor.Core.Ldap.LangFeatures; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; namespace Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; @@ -28,7 +27,7 @@ private static UserIdentityFormat GetIdentityTypeByIdentity(string identity) var id = identity.ToLower(); - if (id.Contains("\\")) + if (id.Contains('\\')) return UserIdentityFormat.NetBiosName; if (id.Contains('=')) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs index c659ebc9..aff2f5f9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Models/UserPassphrase.cs @@ -49,7 +49,7 @@ private UserPassphrase(string raw, string password, string otp, string providerC ProviderCode = providerCode; } - public static UserPassphrase Parse(string rawPwd, PreAuthMode preAuthnMode) + public static UserPassphrase Parse(string rawPwd, PreAuthMode? preAuthnMode) { var hasOtp = TryGetOtpCode(rawPwd, out var otp); if (!hasOtp) @@ -67,7 +67,7 @@ public static UserPassphrase Parse(string rawPwd, PreAuthMode preAuthnMode) return new UserPassphrase(rawPwd, pwd, otp, provCode); } - private static string GetPassword(string rawPwd, PreAuthMode preAuthnMode, bool hasOtp) + private static string GetPassword(string rawPwd, PreAuthMode? preAuthnMode, bool hasOtp) { var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; switch (preAuthnMode) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs index 379f363f..15451c3e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineFactory.cs @@ -21,15 +21,16 @@ public RadiusPipelineFactory( _logger = logger; } - public IRadiusPipeline CreatePipeline(ClientConfiguration clientConfig) + public IRadiusPipeline CreatePipeline(IClientConfiguration clientConfig) { var steps = CreatePipelineSteps(clientConfig); LogProviderCreated(clientConfig.Name, steps); return new RadiusPipeline(steps); } - private List CreatePipelineSteps(ClientConfiguration clientConfig) + private List CreatePipelineSteps(IClientConfiguration clientConfig) { + var withLdap = clientConfig.LdapServers?.Count > 0; var steps = new List { CreateStep(), @@ -37,7 +38,7 @@ private List CreatePipelineSteps(ClientConfiguration client CreateStep() }; - if (clientConfig.LdapServers?.Count > 0) + if (withLdap) { steps.Add(CreateStep()); steps.Add(CreateStep()); @@ -60,7 +61,7 @@ private List CreatePipelineSteps(ClientConfiguration client steps.Add(CreateStep()); } - if (ShouldLoadUserGroups(clientConfig)) + if (withLdap && ShouldLoadUserGroups(clientConfig)) { steps.Add(CreateStep()); } @@ -86,7 +87,8 @@ private void LogProviderCreated(string configName, List ste _logger.LogDebug(builder.ToString()); } - private static bool ShouldLoadUserGroups(ClientConfiguration config) => config + private static bool ShouldLoadUserGroups(IClientConfiguration config) => config + .ReplyAttributes != null && config .ReplyAttributes .Values .SelectMany(x => x) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs index f718a7fe..c1e90ba2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/RadiusPipelineProvider.cs @@ -19,7 +19,7 @@ public RadiusPipelineProvider( _logger = logger; } - public IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) + public IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration) { var clientName = clientConfiguration.Name; return _pipelineCache.GetOrAdd(clientName, name => 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 index 4b9e93b6..5e217204 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/AccessGroupsCheckingStep.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index 53390308..2e521692 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/LdapSchemaLoadingStep.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs index bcd1c78d..2d9e73c6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/ProfileLoadingStep.cs @@ -2,7 +2,6 @@ using Multifactor.Core.Ldap.Attributes; using Multifactor.Core.Ldap.Name; using Multifactor.Radius.Adapter.v2.Application.Cache; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; @@ -99,7 +98,7 @@ public Task ExecuteAsync(RadiusPipelineContext context) return profile; } - private static IEnumerable GetAttributes(RadiusPipelineContext 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.LdapConfiguration!.IdentityAttribute)) diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs index d557e221..79a6878e 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/SecondFactorStep.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index e364a7e8..ea7af5d0 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Pipeline/Steps/UserGroupLoadingStep.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index 0662da0b..109a864c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Exceptions/PipelineNotFoundException.cs @@ -9,10 +9,4 @@ public PipelineNotFoundException(string message, string clientName) { ClientName = clientName; } - - public PipelineNotFoundException(string message, string clientName, Exception innerException) - : base(message, innerException) - { - ClientName = clientName; - } } \ No newline at end of file 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 index da4aaa1b..20a1cfe6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs @@ -8,13 +8,13 @@ public class GetReplyAttributesRequest { public string? UserName { get; } public HashSet UserGroups { get; } - public IReadOnlyDictionary ReplyAttributes { get; } + public IReadOnlyDictionary ReplyAttributes { get; } private IReadOnlyCollection Attributes { get; } public GetReplyAttributesRequest( string? userName, HashSet userGroups, - IReadOnlyDictionary replyAttributes, + IReadOnlyDictionary replyAttributes, IReadOnlyCollection userAttributes) { ArgumentNullException.ThrowIfNull(userGroups); diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs index 1b4166bd..b8a164b1 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacket.cs @@ -1,7 +1,6 @@ using System.Net; using System.Text; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; -using Multifactor.Radius.Adapter.v2.Shared; using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs index 331b28ac..60b79bb9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/RadiusPacketHeader.cs @@ -1,5 +1,4 @@ using System.Security.Cryptography; -using Multifactor.Core.Ldap.LangFeatures; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models @@ -18,6 +17,7 @@ public class RadiusPacketHeader public byte Identifier { get; } public RadiusAuthenticator Authenticator { get; } + public RadiusPacketHeader(){} public RadiusPacketHeader(PacketCode code, byte identifier, byte[] authenticator) { ArgumentNullException.ThrowIfNull(authenticator, nameof(authenticator)); 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 index 10135015..e4ef0e35 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs @@ -19,7 +19,7 @@ public class SendAdapterResponseRequest public ResponseInformation ResponseInformation { get; set; } public SharedSecret RadiusSharedSecret { get; set; } public HashSet UserGroups { get; set; } - public IReadOnlyDictionary RadiusReplyAttributes { get; set; } + public IReadOnlyDictionary RadiusReplyAttributes { get; set; } public IReadOnlyCollection Attributes { get; set; } public (int min, int max)? InvalidCredentialDelay { get; set; } 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 index b806a7df..9a24d340 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/IRadiusPacketProcessor.cs @@ -5,5 +5,5 @@ namespace Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; public interface IRadiusPacketProcessor { - Task ProcessPacketAsync(RadiusPacket requestPacket, ClientConfiguration clientConfiguration); + Task ProcessPacketAsync(RadiusPacket requestPacket, IClientConfiguration clientConfiguration); } \ 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 index a7a83810..1dd3c276 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Services/RadiusPacketProcessor.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -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.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Exceptions; @@ -26,13 +25,11 @@ public RadiusPacketProcessor( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task ProcessPacketAsync(RadiusPacket requestPacket, ClientConfiguration clientConfiguration) + public async Task ProcessPacketAsync(RadiusPacket requestPacket, IClientConfiguration clientConfiguration) { - if (requestPacket == null) - throw new ArgumentNullException(nameof(requestPacket)); - if (clientConfiguration == null) - throw new ArgumentNullException(nameof(clientConfiguration)); - + ArgumentNullException.ThrowIfNull(requestPacket); + ArgumentNullException.ThrowIfNull(clientConfiguration); + _logger.LogDebug("Start processing '{PacketType}' packet for client '{ClientName}'.", requestPacket.Code, clientConfiguration.Name); @@ -45,10 +42,10 @@ public async Task ProcessPacketAsync(RadiusPacket requestPacket, ClientConfigura await TryProcessWithLdapServers(clientConfiguration, requestPacket); } - private async Task TryProcessWithLdapServers(ClientConfiguration clientConfiguration, RadiusPacket requestPacket) + private async Task TryProcessWithLdapServers(IClientConfiguration clientConfiguration, RadiusPacket requestPacket) { - bool processedSuccessfully = false; - Exception lastException = null; + var processedSuccessfully = false; + Exception? lastException = null; foreach (var serverConfig in clientConfiguration.LdapServers) { @@ -81,9 +78,9 @@ private async Task TryProcessWithLdapServers(ClientConfiguration clientConfigura } private async Task ExecutePipeline( - ClientConfiguration clientConfiguration, + IClientConfiguration clientConfiguration, RadiusPacket requestPacket, - LdapServerConfiguration? ldapServerConfiguration = null) + ILdapServerConfiguration? ldapServerConfiguration = null) { if (ldapServerConfiguration != null) { @@ -114,7 +111,6 @@ private async Task ExecutePipeline( } catch (PipelineNotFoundException ex) { - // Не логируем как Warning, т.к. это фатальная ошибка конфигурации _logger.LogError(ex, "Pipeline configuration error for client {ClientName}", clientConfiguration.Name); throw; } @@ -129,13 +125,13 @@ private async Task ExecutePipeline( } private static RadiusPipelineContext CreatePipelineContext( - ClientConfiguration clientConfiguration, + IClientConfiguration clientConfiguration, RadiusPacket requestPacket, - LdapServerConfiguration? ldapServerConfiguration = null) + ILdapServerConfiguration? ldapServerConfiguration = null) { var password = requestPacket.TryGetUserPassword(); - var passphrase = UserPassphrase.Parse(password, clientConfiguration.PreAuthenticationMethod.Value); + var passphrase = UserPassphrase.Parse(password, clientConfiguration.PreAuthenticationMethod); var context = new RadiusPipelineContext(requestPacket, clientConfiguration, ldapServerConfiguration) { @@ -145,7 +141,7 @@ private static RadiusPipelineContext CreatePipelineContext( return context; } - private IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) + private IRadiusPipeline GetPipeline(IClientConfiguration clientConfiguration) { var pipeline = _pipelineProvider.GetPipeline(clientConfiguration); if (pipeline is null) @@ -158,7 +154,7 @@ private IRadiusPipeline GetPipeline(ClientConfiguration clientConfiguration) return pipeline; } - private static bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, ClientConfiguration clientConfiguration) + private static bool ShouldProcessWithoutLdap(RadiusPacket requestPacket, IClientConfiguration clientConfiguration) { if (clientConfiguration.LdapServers.Count <= 0) return true; 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 b3a0a864..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2EClientConfigurationsProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 1b556ad0..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestBase.cs +++ /dev/null @@ -1,178 +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.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.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; -using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; -using Multifactor.Radius.Adapter.v2.Server; -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 static 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 static 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 9bba64a9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/E2ETestsUtils.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; - -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 29b3554e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestClientConfigsProvider.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.Extensions.Options; - -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 6f336810..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/ConfigLoading/TestRootConfigProvider.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -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 38f594ee..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/E2ERadiusConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 30fd615d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/Models/RadiusConfigurationModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 cad01743..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Fixtures/RadiusPacketFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; - -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 ba53167c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/RadiusFixtures.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Udp; - -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 9e0adc88..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessChallengeTests.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 c0420c6d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/AccessRequestAttributesTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 96c40a4e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/BypassWhenApiUnreachableTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 aeee1ebc..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ChangePasswordTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 2d401274..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/FirstFactorTests.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 842e2177..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectory2FaGroupsTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 1b1b97b5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/MultipleActiveDirectoryGroupsTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 e3dfbf6d..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/PreSecondFactorTests.cs +++ /dev/null @@ -1,469 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 2d723032..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/ReplyAttributesTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 5f132ab5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaBypassGroupTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 e07a95aa..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectory2FaGroupTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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 b00d0836..00000000 --- a/src/Multifactor.Radius.Adapter.v2.EndToEndTests/Tests/SingleActiveDirectoryGroupTests.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Moq; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Constants; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures; -using Multifactor.Radius.Adapter.v2.EndToEndTests.Fixtures.Models; - -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.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs index 6942b61c..9f72b581 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/CustomLdapConnectionFactory.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging.Abstractions; using Multifactor.Core.Ldap.Connection; using Multifactor.Core.Ldap.Connection.LdapConnectionFactory; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs index e4ff283e..092460e1 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -9,7 +9,6 @@ using Multifactor.Core.Ldap.LdapGroup.Membership; using Multifactor.Core.Ldap.Name; using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.Ldap; 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; 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 index 3eaf1dc6..80139201 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Http/RoundRobinEndpointSelector.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; 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 index 392ec423..cc231d77 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/Models/AccessRequestDto.cs @@ -24,6 +24,7 @@ public static AccessRequestDto FromQuery(AccessRequestQuery query) Phone = query.Phone, PassCode = query.PassCode, CalledStationId = query.CalledStationId, + CallingStationId = query.CallingStationId, Capabilities = new Capabilities{ InlineEnroll = true }, GroupPolicyPreset = new GroupPolicyPreset{ SignUpGroups = query.SignUpGroups } }; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs index 2d68bfa2..8b6f856c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Multifactor/MultifactorApi.cs @@ -133,7 +133,6 @@ private async Task ProcessResponseAsync( } var content = await response.Content.ReadAsStringAsync(cancellationToken); - var apiResponse = JsonSerializer.Deserialize>( content, _jsonOptions); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs index 9690673f..6225b4de 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/PacketHandler/RadiusUdpAdapter.cs @@ -54,7 +54,6 @@ public async Task Handle(UdpReceiveResult udpPacket) } var requestPacket = _radiusPacketService.ParsePacket(payload, new SharedSecret(clientConfiguration.RadiusSharedSecret)); - Console.WriteLine(Encoding.UTF8.GetString(requestPacket.Authenticator.Value)); requestPacket.ProxyEndpoint = proxyEndpoint; requestPacket.RemoteEndpoint = remoteEndpoint; @@ -97,9 +96,9 @@ private static bool IsProxyProtocol(byte[] payload, out IPEndPoint sourceEndpoin return true; } - private ClientConfiguration? GetClientConfig(UdpReceiveResult udpPacket) + private IClientConfiguration? GetClientConfig(UdpReceiveResult udpPacket) { - ClientConfiguration? clientConfiguration = null; + IClientConfiguration? clientConfiguration = null; if (_radiusPacketService.TryGetNasIdentifier(udpPacket.Buffer, out var nasIdentifier)) clientConfiguration = _serviceConfiguration.GetClientConfiguration(nasIdentifier); clientConfiguration ??= _serviceConfiguration.GetClientConfiguration(udpPacket.RemoteEndPoint.Address); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs index d6189a96..ea4a3e6b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -18,7 +18,7 @@ public CustomUdpClient( Throw.IfNull(endPoint, nameof(endPoint)); _logger = logger; - _udpClient = new UdpClient(); + _udpClient = new UdpClient(endPoint); _logger.LogInformation("UDP client initialized on {Endpoint}", endPoint); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs index 08a8c2a4..0a15ad06 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -1,4 +1,9 @@ -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +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. @@ -7,7 +12,69 @@ public 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); + } + + public static InvalidConfigurationException RequiredFor(Expression> propertySelector, string filePath) + { + const string message = "Property '{prop}' is required. Config name: '{1}'"; + return For(c => propertySelector, message, filePath); ; + } + + 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 index 54688421..ec9c5f5b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -1,77 +1,196 @@ +using System.Net; using System.Reflection; -using Microsoft.Extensions.Logging; +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.Parser; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; +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.Shared.Extensions; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations; public class ConfigurationLoader : IConfigurationLoader { - private readonly IConfigurationParser _parser; + private readonly IRadiusDictionary _dictionary; - public ConfigurationLoader( - IConfigurationParser parser) + public ConfigurationLoader(IRadiusDictionary dictionary) { - _parser = parser; + _dictionary = dictionary; } public ServiceConfiguration Load() { - return Task.Run(() => LoadAsync(CancellationToken.None)).GetAwaiter().GetResult(); - } - - public async Task LoadAsync(CancellationToken cancellationToken) - { - var rootConfig = await LoadRootConfigurationAsync(cancellationToken); - var clients = await LoadClientConfigurationsAsync(cancellationToken); - var serviceConfig = new ServiceConfiguration + var rootConfigPath = GetRootConfigPath(); + var rootConfig = LoadRootConfiguration(rootConfigPath); + var clients = LoadClientConfigurations(rootConfigPath); + + return new ServiceConfiguration { RootConfiguration = rootConfig, - ClientsConfigurations = clients + ClientsConfigurations = clients, + SingleClientMode = clients.Count == 1 }; - return serviceConfig; } - private async Task LoadRootConfigurationAsync(CancellationToken ct) + private static string GetRootConfigPath() { var assemblyLocation = Assembly.GetEntryAssembly()?.Location; - var configPath = $"{assemblyLocation}.config"; - + return $"{assemblyLocation}.config"; + } + + private RootConfiguration LoadRootConfiguration(string configPath) + { if (!File.Exists(configPath)) throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); - return await _parser.ParseRootConfigAsync(configPath, ct); + var config = ReadConfiguration(configPath); + return RootConfiguration.FromConfiguration(config); } - private async Task> LoadClientConfigurationsAsync( - CancellationToken ct) + private List LoadClientConfigurations(string rootConfigPath) { var clientsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "clients"); - var clients = new List(); if (!Directory.Exists(clientsPath)) { - return clients; + var clientConfig = ParseClientConfiguration(rootConfigPath); + return [clientConfig]; + } + + var clientConfigFiles = Directory.GetFiles(clientsPath, "*.config"); + + if (clientConfigFiles.Length == 0) + { + var clientConfig = ParseClientConfiguration(rootConfigPath); + return [clientConfig]; + } + + return clientConfigFiles + .Select(ParseClientConfiguration) + .ToList(); + } + + private ClientConfiguration ParseClientConfiguration(string filePath) + { + var prefix = GetConfigPrefix(filePath); + var config = ReadConfiguration(filePath, prefix); + + var clientConfig = ClientConfiguration.FromConfiguration(config); + clientConfig.ReplyAttributes = ParseReplyAttributes(config.RadiusReply); + clientConfig.LdapServers = config.LdapServers.Select(LdapServerConfiguration.FromConfiguration).ToList(); + + + return clientConfig; + } + + private static ConfigurationFile 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) + .ToArray(); + + result[group.Key] = attributes; } - var configFiles = Directory.GetFiles(clientsPath, "*.config"); - if (configFiles.Length == 0) + return result; + } + + private IRadiusReplyAttribute CreateReplyAttribute(RadiusAttributeItem item) + { + var attribute = new RadiusReplyAttribute(); + + if (bool.TryParse(item.Sufficient, out var sufficient)) { - var assemblyLocation = Assembly.GetEntryAssembly()?.Location; - var configPath = $"{assemblyLocation}.config"; + attribute.Sufficient = sufficient; + } - if (!File.Exists(configPath)) - throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); + if (!string.IsNullOrWhiteSpace(item.From)) + { + attribute.Name = item.From; + } + else if (!string.IsNullOrWhiteSpace(item.Value)) + { + attribute.Value = ParseRadiusReplyValue(item.Name!, item.Value); - return [await _parser.ParseClientConfigAsync(configPath, ct)]; + if (!string.IsNullOrWhiteSpace(item.When)) + { + ParseWhenCondition(item.When, attribute); + } } - foreach (var file in configFiles) + + 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) { - var clientDto = await _parser.ParseClientConfigAsync(file, ct); - clients.Add(clientDto); + 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; - return clients; + 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 index f1bb90bb..79861009 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/IConfigurationLoader.cs @@ -4,6 +4,5 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; public interface IConfigurationLoader { - ServiceConfiguration Load(); - Task LoadAsync(CancellationToken cancellationToken); + ServiceConfiguration Load(); } \ 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..f447030a --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs @@ -0,0 +1,82 @@ +using System.Net; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +public 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 IPAddress? RadiusClientIp { get; set; } + public string RadiusClientNasIdentifier { get; set; } + public string RadiusSharedSecret { get; set; } + public IPEndPoint[] NpsServerEndpoints { get; set; } + public TimeSpan NpsServerTimeout { get; set; } + + public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } + + public PreAuthMode? PreAuthenticationMethod { get; set; } + public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; + public (int min, int max)? 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(ConfigurationFile configurationFile) + { + ArgumentNullException.ThrowIfNull(configurationFile); + var dto = new ClientConfiguration + { + Name = configurationFile.FileName, + MultifactorNasIdentifier = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorNasIdentifier) ? configurationFile.AppSettings.MultifactorNasIdentifier + : throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.MultifactorNasIdentifier, configurationFile.FileName), + MultifactorSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorSharedSecret) ? configurationFile.AppSettings.MultifactorNasIdentifier + : throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.MultifactorSharedSecret, configurationFile.FileName), + SignUpGroups = ConfigurationValueProcessor.TryParseStringList(configurationFile.AppSettings.SignUpGroups, out var list) ? list : [], + BypassSecondFactorWhenApiUnreachable = configurationFile.AppSettings.BypassSecondFactorWhenApiUnreachable, + RadiusClientIp = ConfigurationValueProcessor.TryParseIpAddress(configurationFile.AppSettings.RadiusClientIp, out var address) ? address : null, + RadiusClientNasIdentifier = configurationFile.AppSettings.RadiusClientNasIdentifier, + RadiusSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.RadiusSharedSecret) ? configurationFile.AppSettings.RadiusSharedSecret + : throw InvalidConfigurationException.For(c => c.AppSettings.RadiusSharedSecret, "Property '{prop}' is required. Config name: '{1}'", configurationFile.FileName), + NpsServerEndpoints = ConfigurationValueProcessor.TryParseEndpoints(configurationFile.AppSettings.NpsServerEndpoints, out var npsServerEndpoints) + ? npsServerEndpoints : [], + NpsServerTimeout = ConfigurationValueProcessor.TryParseTimeout(configurationFile.AppSettings.NpsServerTimeout, out var timeout) ? timeout.Value : TimeSpan.Parse("00:00:05"), + Privacy = ConfigurationValueProcessor.TryParsePrivacyModeWithFields(configurationFile.AppSettings.Privacy, out var privacy) ? privacy : new(PrivacyMode.None, []), + PreAuthenticationMethod = ConfigurationValueProcessor.TryParseEnum(configurationFile.AppSettings.PreAuthenticationMethod, out var mode) ? mode : PreAuthMode.None, + AuthenticationCacheLifetime = ConfigurationValueProcessor.TryParseTimeSpan(configurationFile.AppSettings.AuthenticationCacheLifetime, out var span) ? span : TimeSpan.Zero, + CallingStationIdAttribute = configurationFile.AppSettings.CallingStationIdAttribute, + IpWhiteList = ConfigurationValueProcessor.TryParseIpRanges(configurationFile.AppSettings.IpWhiteList, out var ipWhiteList) ? ipWhiteList : [], + InvalidCredentialDelay = ConfigurationValueProcessor.TryParseDelaySettings(configurationFile.AppSettings.InvalidCredentialDelay, out var tuple) + ? tuple + : null + }; + + var firstFactorAuthenticationSource = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.FirstFactorAuthenticationSource) ? configurationFile.AppSettings.FirstFactorAuthenticationSource : + throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.FirstFactorAuthenticationSource, configurationFile.FileName); + dto.FirstFactorAuthenticationSource = ConfigurationValueProcessor.TryParseEnum(firstFactorAuthenticationSource, out var source) + ? source + : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{1}'", configurationFile.FileName); ; + + var adapterClientEndpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint) ? configurationFile.AppSettings.AdapterClientEndpoint : + throw InvalidConfigurationException.For(c => c.AppSettings.AdapterClientEndpoint, "Property '{prop}' is required. Config name: '{1}'", configurationFile.FileName); + dto.AdapterClientEndpoint = + ConfigurationValueProcessor.TryParseEndpoint(adapterClientEndpoint, out var endpoint) + ? endpoint! : + throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{1}'", configurationFile.FileName); ; + return dto; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs new file mode 100644 index 00000000..019e4104 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs @@ -0,0 +1,156 @@ +using System.ComponentModel; +using System.Xml.Serialization; +using Microsoft.Extensions.Configuration; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +public class ConfigurationFile +{ + public ConfigurationFile() { } + public string FileName { get; set; } + public AppSettingsSection AppSettings { get; set; } = new(); + + public List LdapServers { get; set; } = new(); + + public RadiusReplySection RadiusReply { get; set; } = new(); +} + +public 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 NpsServerEndpoints { 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; } +} + +public 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 NestedGroupsBaseDns { 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; } +} + +public class RadiusReplySection +{ + [XmlArray("Attributes")] + [XmlArrayItem("add")] + public List Attributes { get; set; } +} + +public 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/Dictionary/Attributes/DictionaryAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryAttribute.cs similarity index 98% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/DictionaryAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryAttribute.cs index 5871a693..802d4b9d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/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.Infrastructure.Configurations.Dictionary.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class DictionaryAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/DictionaryVendorAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryVendorAttribute.cs similarity index 98% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/DictionaryVendorAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/DictionaryVendorAttribute.cs index 7f3d9b1d..f33783be 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/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.Infrastructure.Configurations.Dictionary.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class DictionaryVendorAttribute : DictionaryAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/VendorSpecificAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/VendorSpecificAttribute.cs similarity index 98% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/Attributes/VendorSpecificAttribute.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/Attributes/VendorSpecificAttribute.cs index e2902960..187d2ab4 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/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.Infrastructure.Configurations.Dictionary.Attributes +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes { public class VendorSpecificAttribute { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/IRadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs similarity index 94% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/IRadiusDictionary.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs index 43cddef5..29988927 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/IRadiusDictionary.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/IRadiusDictionary.cs @@ -1,6 +1,6 @@ -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary { public interface IRadiusDictionary { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs similarity index 96% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs index 5e647612..9dac683c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Dictionary/RadiusDictionary.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/Dictionary/RadiusDictionary.cs @@ -1,9 +1,9 @@ using System.Text; using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using DictionaryAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryAttribute; -using DictionaryVendorAttribute = Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes.DictionaryVendorAttribute; +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.Dictionary +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary { public class RadiusDictionary : IRadiusDictionary { 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..a1078c18 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs @@ -0,0 +1,101 @@ +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; + +public 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; } + public bool TrustedDomainsEnabled { get; init; } + public bool AlternativeSuffixesEnabled { get; init; } + 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) + { + var dto = new LdapServerConfiguration + { + ConnectionString = !string.IsNullOrWhiteSpace(ldapServerSection.ConnectionString) ? ldapServerSection.ConnectionString : + throw InvalidConfigurationException.RequiredFor(section => section.LdapServers[0].ConnectionString, nameof(ldapServerSection)), + Username = !string.IsNullOrWhiteSpace(ldapServerSection.Username) ? ldapServerSection.Username : + throw InvalidConfigurationException.RequiredFor(section => section.LdapServers[0].Username, nameof(ldapServerSection)), + Password = !string.IsNullOrWhiteSpace(ldapServerSection.Password) ? ldapServerSection.Password : + throw InvalidConfigurationException.RequiredFor(section => section.LdapServers[0].Password, nameof(ldapServerSection)), + BindTimeoutSeconds = ldapServerSection.BindTimeoutSeconds ?? 30, + AccessGroups = + ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.AccessGroups, + out var accessGroups) + ? accessGroups + : [], + SecondFaGroups = + ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.SecondFaGroups, + out var secondFaGroups) + ? secondFaGroups + : [], + SecondFaBypassGroups = + ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.SecondFaBypassGroups, out var secondFaBypassGroups) + ? secondFaBypassGroups + : [], + LoadNestedGroups =ldapServerSection.LoadNestedGroups, + NestedGroupsBaseDns = + ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDns, out var nestedGroupsBaseDns) + ? nestedGroupsBaseDns + : [], + AuthenticationCacheGroups = + ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.AuthenticationCacheGroups, out var authenticationCacheGroups) + ? authenticationCacheGroups + : [], + PhoneAttributes = + ConfigurationValueProcessor.TryParseStringList(ldapServerSection.PhoneAttributes, + out var phoneAttributes) + ? phoneAttributes + : [], + IdentityAttribute = ldapServerSection.IdentityAttribute ?? "sAMAccountName", + RequiresUpn = ldapServerSection.RequiresUpn, + TrustedDomainsEnabled = ldapServerSection.TrustedDomainsEnabled, + AlternativeSuffixesEnabled =ldapServerSection.AlternativeSuffixesEnabled, + IncludedDomains = + ConfigurationValueProcessor.TryParseStringList(ldapServerSection.IncludedDomains, + out var includedDomains) + ? includedDomains + : [], + ExcludedDomains = + ConfigurationValueProcessor.TryParseStringList(ldapServerSection.ExcludedDomains, + out var excludedDomains) + ? excludedDomains + : [], + IncludedSuffixes = + ConfigurationValueProcessor.TryParseStringList(ldapServerSection.IncludedSuffixes, + out var includedSuffixes) + ? includedSuffixes + : [], + ExcludedSuffixes = + ConfigurationValueProcessor.TryParseStringList(ldapServerSection.ExcludedSuffixes, + out var excludedSuffixes) + ? excludedSuffixes + : [], + BypassSecondFactorWhenApiUnreachableGroups = ConfigurationValueProcessor.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..09eabd7f --- /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; + +public 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(ConfigurationFile configurationFile) + { + ArgumentNullException.ThrowIfNull(configurationFile); + var conf = new RootConfiguration + { + MultifactorApiProxy = configurationFile.AppSettings.MultifactorApiProxy, + MultifactorApiTimeout = ConfigurationValueProcessor.TryParseTimeout( + configurationFile.AppSettings.MultifactorApiTimeout, out var span) + ? span!.Value : TimeSpan.FromSeconds(65), + LoggingFormat = configurationFile.AppSettings.LoggingFormat, + SyslogUseTls = configurationFile.AppSettings.SyslogUseTls, + 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.RequiredFor(prop => prop.AppSettings.MultifactorApiUrl, configurationFile.FileName); + conf.MultifactorApiUrls = ConfigurationValueProcessor.TryParseUrls(urls, out var parsedUrls) + ? parsedUrls + : throw new InvalidConfigurationException($"Invalid 'multifactor-api-url': '{urls}'", configurationFile.FileName); + var endpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterServerEndpoint) ? configurationFile.AppSettings.AdapterServerEndpoint : throw new InvalidConfigurationException(nameof(conf.AdapterServerEndpoint)); + + conf.AdapterServerEndpoint = ConfigurationValueProcessor.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/ConfigurationValueProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs new file mode 100644 index 00000000..685adffc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs @@ -0,0 +1,298 @@ +using System.Globalization; +using System.Net; +using Multifactor.Core.Ldap.Name; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; +using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; +using NetTools; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; + +public static class ConfigurationValueProcessor +{ + 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 IPAddress? result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (IPAddress.TryParse(value, out var ipAddress)) + { + result = ipAddress; + return true; + } + + return false; + } + + 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 (PrivacyMode Mode, string[] Fields) result) + { + result = (PrivacyMode.None, []); + + 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 = (mode, []); + return true; + } + + var fields = parts[1].Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(f => f.Trim()) + .Distinct() + .ToArray(); + + result = (mode, fields); + return true; + } + + public static bool TryParseDelaySettings(string? value, out (int min, int max) result) + { + result = (0, 0); + + if (string.IsNullOrWhiteSpace(value)) + return false; + + if (int.TryParse(value, out var delay)) + { + if (delay < 0) + return false; + + result = (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 = (values[0], values[1]); + return true; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs deleted file mode 100644 index 1f8c96de..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/IConfigurationParser.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; - -public interface IConfigurationParser -{ - Task ParseRootConfigAsync(string filePath, CancellationToken ct); - Task ParseClientConfigAsync(string filePath, CancellationToken ct); -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs deleted file mode 100644 index c733c831..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ValueParser.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Globalization; -using System.Net; -using Multifactor.Core.Ldap.Name; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; -using NetTools; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; - -public static class ValueParser -{ - public static T ParseEnum(string? value, T defaultValue = default, bool required = false) where T : struct - { - if (string.IsNullOrWhiteSpace(value)) - { - return required ? throw new InvalidConfigurationException($"Enum value of type {typeof(T).Name} is required") : defaultValue; - } - - return Enum.TryParse(value, true, out var result) ? result - : throw new InvalidConfigurationException($"Invalid value '{value}' for enum {typeof(T).Name}"); - } - - public static bool ParseBool(string? value, bool defaultValue) - { - if (string.IsNullOrWhiteSpace(value)) - return defaultValue; - - return bool.TryParse(value, out var result) ? result : defaultValue; - } - - public static int ParseInt(string? value, int defaultValue) - { - if (string.IsNullOrWhiteSpace(value)) - return defaultValue; - - return int.TryParse(value, out var result) ? result : defaultValue; - } - - public static TimeSpan ParseTimeSpan(string? value, TimeSpan? defaultValue = null) - { - if (string.IsNullOrWhiteSpace(value)) - return defaultValue ?? TimeSpan.Zero; - - if (TimeSpan.TryParse(value, out var result)) - return result; - - throw new InvalidConfigurationException($"Invalid time span format: '{value}'"); - } - - public static TimeSpan ParseTimeout(string? value, TimeSpan defaultValue) - { - if (string.IsNullOrWhiteSpace(value)) - return defaultValue; - - var forced = value.EndsWith('!'); - if (forced) - value = value.TrimEnd('!'); - - if (!TimeSpan.TryParseExact(value, @"hh\:mm\:ss", null, TimeSpanStyles.None, out var result)) - return defaultValue; - - if (result == TimeSpan.Zero) - return Timeout.InfiniteTimeSpan; - - // Логирование если timeout слишком маленький - var recommendedMin = TimeSpan.FromSeconds(65); - if (result < recommendedMin) - { - if (forced) - { - StartupLogger.Warning( - "Timeout {Timeout}s is less than recommended minimum {Recommended}s", - result.TotalSeconds, recommendedMin.TotalSeconds); - } - else - { - StartupLogger.Warning( - "Timeout {Timeout}s is less than recommended minimum {Recommended}s. Use 'value!' to force", - result.TotalSeconds, recommendedMin.TotalSeconds); - result = recommendedMin; - } - } - - return result; - } - - public static IPEndPoint? ParseEndpoint(string? value, bool required = false) - { - if (string.IsNullOrWhiteSpace(value)) - { - return required - ? throw new InvalidConfigurationException("Endpoint is required") : null; - } - - return IPEndPoint.TryParse(value, out var endpoint) ? endpoint - : throw new InvalidConfigurationException($"Invalid endpoint format: '{value}'"); - } - - public static IPEndPoint[] ParseEndpoints(string? value, char separator = ';', bool required = false) - { - if (string.IsNullOrWhiteSpace(value)) - { - return required - ? throw new InvalidConfigurationException("Endpoints is required") : []; - } - - return value.Split(separator).Select(endpoint => IPEndPoint.TryParse(endpoint, out var endpointResult) ? endpointResult - : throw new InvalidConfigurationException($"Invalid endpoint format: '{endpoint}'")).ToArray(); - - } - - public static IPAddress? ParseIpAddress(string? value, bool required = false) - { - return IPAddress.TryParse(value, out var result) ? result - : throw new InvalidConfigurationException($"Invalid IP address format: '{value}'"); - } - - public static IReadOnlyList ParseUrls(string? value, bool required = false) - { - if (string.IsNullOrWhiteSpace(value)) - { - return required ? throw new InvalidConfigurationException("URLs are required") : []; - } - - 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)) - { - urls.Add(uri); - } - else - { - throw new InvalidConfigurationException($"Invalid URL format: '{trimmed}'"); - } - } - - return urls; - } - - public static IReadOnlyList ParseIpRanges(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return []; - - 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)) - { - ranges.Add(range); - } - else - { - throw new InvalidConfigurationException($"Invalid IP address range: '{trimmed}'"); - } - } - - return ranges; - } - - public static IReadOnlyList ParseDistinguishedNames(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return []; - - 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 ex) - { - throw new InvalidConfigurationException($"Invalid distinguished name: '{part}'", ex); - } - } - - return names; - } - - public static IReadOnlyList ParseStringList(string? value, char separator = ';') - { - if (string.IsNullOrWhiteSpace(value)) - return []; - - return value.Split(separator, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .Where(s => !string.IsNullOrEmpty(s)) - .ToList(); - } - - - public static (PrivacyMode Mode, string[] Fields) ParsePrivacyModeWithFields(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return (PrivacyMode.None, []); - - var parts = value.Split(':', 2); - var mode = ParseEnum(parts[0], PrivacyMode.None); - - if (parts.Length == 1) - return (mode, []); - - var fields = parts[1].Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(f => f.Trim()) - .Distinct() - .ToArray(); - - return (mode, fields); - } - - public static (int min, int max) ParseDelaySettings(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return new (0, 0); - } - - if (int.TryParse(value, out var delay)) - { - return delay < 0 ? throw new InvalidConfigurationException($"Invalid delay setting: '{value}'") - : new (delay, delay); - } - - var splitted = value.Split(['-'], StringSplitOptions.RemoveEmptyEntries); - if (splitted.Length != 2) throw new InvalidConfigurationException($"Invalid delay setting: '{value}'"); - - var values = splitted.Select(x => int.TryParse(x, out var d) ? d : -1).ToArray(); - return values.Any(x => x < 0) ? throw new InvalidConfigurationException($"Invalid delay setting: '{value}'") - : new (values[0], values[1]); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs deleted file mode 100644 index f6ca2448..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/XmlConfigurationParser.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.Net; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models.Enum; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; -using Multifactor.Radius.Adapter.v2.Shared.Extensions; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; - -public class XmlConfigurationParser : IConfigurationParser -{ - private readonly IRadiusDictionary _dictionary; - const string _commonPrefix = "RAD_"; - - public XmlConfigurationParser( - IRadiusDictionary dictionary) - { - _dictionary = dictionary; - } - - public async Task ParseRootConfigAsync(string filePath, CancellationToken ct) - { - var xml = await XmlReader.ReadAsync(filePath, ct); - var settingsXml = XmlReader.ExtractAppSettings(xml); - - var settingsEnv = EnvironmentReader.ReadEnvironments("RAD_APPSETTINGS"); - - return new RootConfiguration - { - MultifactorApiUrls = ValueParser.ParseUrls(GetRawValue("multifactor-api-url", settingsEnv, settingsXml, true), required: true), - MultifactorApiProxy = GetRawValue("multifactor-api-proxy", settingsEnv, settingsXml), - MultifactorApiTimeout = ValueParser.ParseTimeout(GetRawValue("multifactor-api-timeout", settingsEnv, settingsXml), - TimeSpan.FromSeconds(65)), - AdapterServerEndpoint = ValueParser.ParseEndpoint(GetRawValue("adapter-server-endpoint", settingsEnv, settingsXml, true), required: true), - LoggingFormat = GetRawValue("logging-format", settingsEnv, settingsXml), - SyslogUseTls = ValueParser.ParseBool(GetRawValue("syslog-use-tls", settingsEnv, settingsXml), false), - SyslogServer = GetRawValue("syslog-server", settingsEnv, settingsXml), - SyslogFormat = GetRawValue("syslog-format", settingsEnv, settingsXml), - SyslogFacility = GetRawValue("syslog-facility", settingsEnv, settingsXml), - SyslogAppName = GetRawValue("syslog-app-name", settingsEnv, settingsXml) ?? "multifactor-radius", - SyslogFramer = GetRawValue("syslog-framer", settingsEnv, settingsXml), - SyslogOutputTemplate = GetRawValue("syslog-output-template", settingsEnv, settingsXml), - ConsoleLogOutputTemplate = GetRawValue("console-log-output-template", settingsEnv, settingsXml), - FileLogOutputTemplate = GetRawValue("file-log-output-template", settingsEnv, settingsXml), - LogFileMaxSizeBytes = ValueParser.ParseInt(GetRawValue("log-file-max-size-bytes", settingsEnv, settingsXml), 1073741824), - LoggingLevel = GetRawValue("logging-level", settingsEnv, settingsXml) - }; - } - - public async Task ParseClientConfigAsync(string filePath, CancellationToken ct) - { - var xml = await XmlReader.ReadAsync(filePath, ct); - var settingsXml = XmlReader.ExtractAppSettings(xml); - - var prefix = TransformName(filePath); - var settingsEnv = EnvironmentReader.ReadEnvironments($"{_commonPrefix}{prefix}APPSETTINGS"); - - var dto = new ClientConfiguration - { - Name = Path.GetFileNameWithoutExtension(filePath), - MultifactorNasIdentifier = GetRawValue("multifactor-nas-identifier", settingsEnv, settingsXml, true), - MultifactorSharedSecret = GetRawValue("multifactor-shared-secret", settingsEnv, settingsXml, true), - SignUpGroups = ValueParser.ParseStringList(GetRawValue("sign-up-group", settingsEnv, settingsXml)), - BypassSecondFactorWhenApiUnreachable = ValueParser.ParseBool(GetRawValue("bypass-second-factor-when-api-unreachable", settingsEnv, settingsXml), true), - FirstFactorAuthenticationSource = ValueParser.ParseEnum(GetRawValue("first-factor-authentication-source", settingsEnv, settingsXml, true), required: true), - AdapterClientEndpoint = ValueParser.ParseEndpoint(GetRawValue("adapter-client-endpoint", settingsEnv, settingsXml, true), required: true), - RadiusClientIp = ValueParser.ParseIpAddress(GetRawValue("radius-client-ip", settingsEnv, settingsXml)), - RadiusClientNasIdentifier = GetRawValue("radius-client-nas-identifier", settingsEnv, settingsXml), - RadiusSharedSecret = GetRawValue("radius-shared-secret", settingsEnv, settingsXml, true), - NpsServerEndpoints = ValueParser.ParseEndpoints(GetRawValue("nps-server-endpoint", settingsEnv, settingsXml, true), required: true), - NpsServerTimeout = ValueParser.ParseTimeout(GetRawValue("nps-server-timeout", settingsEnv, settingsXml), TimeSpan.Parse("00:00:05")), - Privacy = ValueParser.ParsePrivacyModeWithFields(GetRawValue("privacy-mode", settingsEnv, settingsXml)), - PreAuthenticationMethod = ValueParser.ParseEnum(GetRawValue("pre-authentication-method", settingsEnv, settingsXml), PreAuthMode.None), - AuthenticationCacheLifetime = ValueParser.ParseTimeSpan(GetRawValue("authentication-cache-lifetime", settingsEnv, settingsXml)), - CallingStationIdAttribute = GetRawValue("calling-station-id-attribute", settingsEnv, settingsXml), - IpWhiteList = ValueParser.ParseIpRanges(GetRawValue("ip-white-list", settingsEnv, settingsXml)), - InvalidCredentialDelay = ValueParser.ParseDelaySettings(GetRawValue("invalid-credential-delay", settingsEnv, settingsXml)), - ReplyAttributes = ParseReplyAttributes(xml) - }; - - dto.LdapServers = ParseLdapServers(xml, dto.Name); - - return dto; - } - - private List ParseLdapServers(XDocument xml, string configName) - { - var servers = new List(); - var ldapElements = XmlReader.GetLdapServerElements(xml); - - if (ldapElements == null || ldapElements.Count == 0) - return servers; - - servers.AddRange(ldapElements.Select(element => new LdapServerConfiguration - { - ConnectionString = element.Attribute("connection-string")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), - Username = element.Attribute("username")?.Value ?? throw new InvalidConfigurationException("LDAP username is required"), - Password = element.Attribute("password")?.Value ?? throw new InvalidConfigurationException("LDAP password is required"), - BindTimeoutSeconds = ValueParser.ParseInt(element.Attribute("bind-timeout-in-seconds")?.Value, 30), - AccessGroups = ValueParser.ParseDistinguishedNames(element.Attribute("access-groups")?.Value), - SecondFaGroups = ValueParser.ParseDistinguishedNames(element.Attribute("second-fa-groups")?.Value), - SecondFaBypassGroups = ValueParser.ParseDistinguishedNames(element.Attribute("second-fa-bypass-groups")?.Value), - LoadNestedGroups = ValueParser.ParseBool(element.Attribute("load-nested-groups")?.Value, true), - NestedGroupsBaseDns = ValueParser.ParseDistinguishedNames(element.Attribute("nested-groups-base-dn")?.Value), - AuthenticationCacheGroups = ValueParser.ParseDistinguishedNames(element.Attribute("authentication-cache-groups")?.Value), - PhoneAttributes = ValueParser.ParseStringList(element.Attribute("phone-attributes")?.Value), - IdentityAttribute = element.Attribute("identity-attribute")?.Value ?? "sAMAccountName", - RequiresUpn = ValueParser.ParseBool(element.Attribute("requires-upn")?.Value, false), - TrustedDomainsEnabled = ValueParser.ParseBool(element.Attribute("enable-trusted-domains")?.Value, false), - AlternativeSuffixesEnabled = ValueParser.ParseBool(element.Attribute("enable-alternative-suffixes")?.Value, false), - IncludedDomains = ValueParser.ParseStringList(element.Attribute("included-domains")?.Value), - ExcludedDomains = ValueParser.ParseStringList(element.Attribute("excluded-domains")?.Value), - IncludedSuffixes = ValueParser.ParseStringList(element.Attribute("included-suffixes")?.Value), - ExcludedSuffixes = ValueParser.ParseStringList(element.Attribute("excluded-suffixes")?.Value), - BypassSecondFactorWhenApiUnreachableGroups = ValueParser.ParseStringList(element.Attribute("bypass-second-factor-when-api-unreachable-groups")?.Value) - })); - - return servers; - } - - private IReadOnlyDictionary ParseReplyAttributes( - XDocument xml) - { - var elements = XmlReader.GetRadiusReplyElements(xml); - if (!elements.Any()) - return new Dictionary(); - - var attributeGroups = elements - .Where(e => e.Attribute("name") != null) - .GroupBy(e => e.Attribute("name")!.Value); - - var result = new Dictionary(); - - foreach (var group in attributeGroups) - { - var attributeName = group.Key; - var attributes = new List(); - - foreach (var element in group) - { - var fromAttr = element.Attribute("from")?.Value; - var valueAttr = element.Attribute("value")?.Value; - var whenAttr = element.Attribute("when")?.Value; - var sufficientAttr = element.Attribute("sufficient")?.Value; - - var sufficient = bool.TryParse(sufficientAttr, out var suff) && suff; - - if (!string.IsNullOrEmpty(fromAttr)) - { - attributes.Add(new RadiusReplyAttribute - { - Name = fromAttr, - Sufficient = sufficient - }); - } - else if (!string.IsNullOrEmpty(valueAttr)) - { - var value = ParseRadiusReplyValue(attributeName, valueAttr); - var clauses = whenAttr.Split(['='], StringSplitOptions.RemoveEmptyEntries); - var conditions = clauses[0] switch - { - "UserGroup" or "UserName" => clauses[1] - .Split([';'], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()).ToList(), - _ => throw new Exception($"Unknown condition '{clauses}'") - }; - var attribute = new RadiusReplyAttribute - { - Value = value, - Sufficient = sufficient - }; - if(clauses[0]=="UserGroup") - attribute.UserGroupCondition = conditions; - else attribute.UserNameCondition = conditions; - attributes.Add(attribute); - } - } - - result[attributeName] = attributes.ToArray(); - } - - return result; - } - - private object ParseRadiusReplyValue(string attributeName, string value) - { - var attribute = _dictionary.GetAttribute(attributeName); - if (string.IsNullOrEmpty(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 => value.ToByteArray(), - _ => throw new Exception($"Unknown type {attribute.Type}") - }; - } - - private static string TransformName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - return string.Empty; - } - name = Regex.Replace(name, @"\s+", string.Empty); - return name; - } - - private static string GetRawValue(string key, IReadOnlyDictionary primary, IReadOnlyDictionary secondary, bool required = false) - { - return primary.TryGetValue(key, out var primaryValue) ? primaryValue - : secondary.TryGetValue(key, out var secondaryValue) ? secondaryValue - : !required ? string.Empty : throw new InvalidConfigurationException($"{key} is required") ; - } -} \ 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..9845ed1b --- /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 AddLegacyXmlConfig( + this IConfigurationBuilder builder, + string path) + { + return builder.Add(new XmlConfigurationSource(path)); + } + + public static IConfigurationBuilder AddPrefixEnvironmentVariables( + 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..7f861ad8 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Configuration; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; + +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; + +public static class ConfigurationReader +{ + public static ConfigurationFile Read(string filePath, string prefix = null) + { + var builder = new ConfigurationBuilder() + .AddLegacyXmlConfig(filePath) + .AddPrefixEnvironmentVariables($"RAD_{prefix}") + .Build(); + + try + { + var config = builder.Get(); + config.FileName = Path.GetFileNameWithoutExtension(filePath); + return config; + } + catch (Exception ex) + { + Console.WriteLine($"\n=== Ошибка при Get(): {ex.Message} ==="); + Console.WriteLine("Подробности: " + ex.InnerException?.Message); + throw; + } + + } +} + \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs deleted file mode 100644 index b22426d2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/EnvironmentReader.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; - -public static class EnvironmentReader -{ - public static IReadOnlyDictionary ReadEnvironments(string? prefix = null) - { - return Environment.GetEnvironmentVariables() - .Cast() - .Where(x => x.Key.ToString().StartsWith(prefix)) - .ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()); - } -} \ 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/Configurations/Reader/XmlReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs deleted file mode 100644 index c0db1a5f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/XmlReader.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Xml; -using System.Xml.Linq; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; - -public static class XmlReader -{ - public static async Task ReadAsync(string filePath, CancellationToken cancellationToken) - { - try - { - await using var stream = File.OpenRead(filePath); - var settings = new XmlReaderSettings - { - Async = true, - IgnoreComments = true, - IgnoreWhitespace = true - }; - - using var xmlReader = System.Xml.XmlReader.Create(stream, settings); - var document = await XDocument.LoadAsync(xmlReader, LoadOptions.None, cancellationToken); - - return document; - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - throw new InvalidConfigurationException($"Failed to read configuration file: {filePath}", ex); - } - } - - public static IReadOnlyDictionary ExtractAppSettings(XDocument xml) - { - var appSettings = xml.Root?.Element("appSettings"); - if (appSettings == null) - return new Dictionary(); - - return appSettings.Elements("add") - .Where(e => e.Attribute("key") != null && e.Attribute("value") != null) - .ToDictionary( - e => e.Attribute("key")!.Value, - e => e.Attribute("value")!.Value, - StringComparer.OrdinalIgnoreCase); - } - - public static IReadOnlyList GetLdapServerElements(XDocument xml) - { - return xml.Root?.Element("ldapServers")?.Elements("ldapServer").ToList() - ?? []; - } - - public static IReadOnlyList GetRadiusReplyElements(XDocument xml) - { - var attributes = xml.Root?.Element("RadiusReply")?.Element("Attributes"); - return attributes?.Elements("add").ToList() ?? []; - } -} \ 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 index 6de0688d..471293a7 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -7,10 +7,8 @@ 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; 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.Pipeline; 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; @@ -20,8 +18,9 @@ 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.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; @@ -48,7 +47,6 @@ public static void AddConfiguration(this IServiceCollection services) dict.Read(); return dict; }); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => @@ -85,7 +83,7 @@ public static void AddMultifactorApi(this IServiceCollection services) .ConfigureHttpClient((serviceProvider, client) => { var config = serviceProvider.GetRequiredService(); - if (config.RootConfiguration.MultifactorApiUrls?.Any() == true) + if (config.RootConfiguration.MultifactorApiUrls.Any()) { var primaryUrl = config.RootConfiguration.MultifactorApiUrls[0]; client.BaseAddress = primaryUrl; @@ -99,13 +97,11 @@ public static void AddMultifactorApi(this IServiceCollection services) .FallbackAsync( fallbackAction: async (outcome, context, cancellationToken) => { - StartupLogger.Error("start"); var urlSelector = serviceProvider.GetRequiredService(); var fallbackUrl = await urlSelector.GetNextEndpointAsync(); var fallbackRequest = request.CloneHttpRequestMessage(); fallbackRequest.RequestUri = new Uri(fallbackUrl, request.RequestUri!.PathAndQuery); - StartupLogger.Error(fallbackRequest.RequestUri.ToString()); var httpClientFactory = serviceProvider.GetRequiredService(); var httpClient = httpClientFactory.CreateClient("multifactor-api"); @@ -114,9 +110,9 @@ public static void AddMultifactorApi(this IServiceCollection services) }, onFallbackAsync: (outcome, context) => { - var logger = serviceProvider.GetRequiredService(); - logger.LogWarning("Primary endpoint failed. Trying fallback. Error: {Error}", - outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); + // var logger = serviceProvider.GetRequiredService(); + // logger.LogWarning("Primary endpoint failed. Trying fallback. Error: {Error}", + // outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); return Task.CompletedTask; }) .WrapAsync(Policy.TimeoutAsync(TimeSpan.FromSeconds(10))) @@ -131,7 +127,7 @@ public static void AddMultifactorApi(this IServiceCollection services) SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 }; - if (config.RootConfiguration.MultifactorApiProxy == null) + if (string.IsNullOrWhiteSpace(config.RootConfiguration.MultifactorApiProxy)) return handler; if (!WebProxyFactory.TryCreateWebProxy(config.RootConfiguration.MultifactorApiProxy, out var webProxy)) @@ -151,8 +147,7 @@ public static void AddAdapterLogging(this IServiceCollection services) services.AddSerilog((provider, loggerConfiguration) => { var serviceConfiguration = provider.GetRequiredService(); - var logger = SerilogLoggerFactory.CreateLogger(serviceConfiguration.RootConfiguration); - Log.Logger = logger; + SerilogLoggerFactory.CreateLogger(loggerConfiguration, serviceConfiguration.RootConfiguration); }); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index 402d81fe..b399040f 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -1,6 +1,7 @@ using Elastic.CommonSchema.Serilog; 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 Serilog; using Serilog.Core; using Serilog.Events; @@ -13,14 +14,13 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Logging; public static class SerilogLoggerFactory { - public static ILogger CreateLogger(RootConfiguration 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(); @@ -40,17 +40,16 @@ public static ILogger CreateLogger(RootConfiguration rootConfiguration) rootConfiguration.SyslogUseTls ); var level = rootConfiguration.LoggingLevel; - if (string.IsNullOrWhiteSpace(level)) - { - // throw new InvalidConfigurationException( - // "'{prop}' element not found. Config name: '{0}'", - // rootConfiguration.ConfigurationName); - } + var skip = string.IsNullOrWhiteSpace(level); + // if (string.IsNullOrWhiteSpace(level)) + // { + // 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( diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs index 1013a200..c66b706a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs @@ -3,8 +3,8 @@ using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Security; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs index 10005f24..e1b711d3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -3,8 +3,8 @@ 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.Configurations.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; +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; @@ -13,8 +13,8 @@ public class RadiusPacketBuilder : IRadiusPacketBuilder { private readonly IRadiusDictionary _radiusDictionary; private readonly IRadiusCryptoProvider _cryptoProvider; - private readonly ILogger _logger; - private readonly IRadiusAttributeSerializer _attributeSerializer; + // private readonly ILogger _logger; + // private readonly IRadiusAttributeSerializer _attributeSerializer; /// @@ -40,8 +40,8 @@ public RadiusPacketBuilder( { _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); - _attributeSerializer = attributeSerializer ?? throw new ArgumentNullException(nameof(attributeSerializer)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + // _attributeSerializer = attributeSerializer ?? throw new ArgumentNullException(nameof(attributeSerializer)); + // _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) @@ -49,6 +49,7 @@ 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) @@ -60,13 +61,13 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) // 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 @@ -81,6 +82,7 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) FillMessageAuthenticator(packetBytesArray, messageAuthenticatorPosition, sharedSecret); } authenticator = _cryptoProvider.CalculateRequestAuthenticator(sharedSecret, packetBytesArray); + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); break; case PacketCode.StatusServer: @@ -98,7 +100,9 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) messageAuthenticatorPosition, sharedSecret, packet.RequestAuthenticator); - } + } + Buffer.BlockCopy(authenticator, 0, packetBytesArray, 4, 16); + break; default: @@ -115,7 +119,7 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) sharedSecret, packet.RequestAuthenticator); } - + if (packet.RequestAuthenticator != null) { authenticator = _cryptoProvider.CalculateResponseAuthenticator( @@ -126,7 +130,7 @@ public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) } break; } - + return packetBytesArray; } @@ -152,24 +156,24 @@ private void FillAttributes(List packetBytes, RadiusAuthenticator authenti { 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) { @@ -179,20 +183,20 @@ private void FillAttributes(List packetBytes, RadiusAuthenticator authenti { 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 /// @@ -220,15 +224,14 @@ private static byte[] GetAttributeValueBytes(object value) 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( diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs index d0ac4daf..34f5ef27 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -2,8 +2,8 @@ using System.Text; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary.Attributes; +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; @@ -11,7 +11,6 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Parsers; public class RadiusAttributeParser : IRadiusAttributeParser { private readonly IRadiusDictionary _radiusDictionary; - private readonly IRadiusCryptoProvider _cryptoProvider; private readonly ILogger _logger; const int VendorSpecific = 26; const int MessageAuthenticator = 80; @@ -19,11 +18,9 @@ public class RadiusAttributeParser : IRadiusAttributeParser public RadiusAttributeParser( IRadiusDictionary radiusDictionary, - IRadiusCryptoProvider cryptoProvider, ILogger logger) { _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); - _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs index 7857b93a..a9e6baa6 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusPacketParser.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs index b3ae8076..41ba8621 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -63,7 +63,6 @@ public async Task SendResponse(SendAdapterResponseRequest request) var responsePacket = BuildResponsePacket(request); - Console.WriteLine(responsePacket.Authenticator.Value.ToString()); await SendResponsePacketAsync(responsePacket, request); LogResponseSent(responsePacket, request); @@ -76,7 +75,6 @@ private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) request.RequestPacket, responsePacketCode); - // Обработка в зависимости от типа ответа switch (responsePacketCode) { case PacketCode.AccessAccept: @@ -96,7 +94,6 @@ private RadiusPacket BuildResponsePacket(SendAdapterResponseRequest request) $"Response packet code {responsePacketCode} is not supported"); } - // Добавляем общие атрибуты AddCommonAttributes(responsePacket, request); return responsePacket; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs index 06c7dad4..0c1d77b5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusAttributeTypeConverter.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Net; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Dictionary; +using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs index a82ce309..f50e5be8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusPacketService.cs @@ -61,8 +61,8 @@ public RadiusPacket ParsePacket(byte[] packetBytes, SharedSecret sharedSecret, R public byte[] SerializePacket(RadiusPacket packet, SharedSecret sharedSecret) { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (sharedSecret == null) throw new ArgumentNullException(nameof(sharedSecret)); + ArgumentNullException.ThrowIfNull(packet); + ArgumentNullException.ThrowIfNull(sharedSecret); try { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs index 0f577626..fcff7bbf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -1,10 +1,9 @@ -// Infrastructure/Radius/Services/RadiusReplyAttributeService.cs - 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.Infrastructure.Configurations.Models; using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; @@ -37,7 +36,6 @@ public IDictionary> GetReplyAttributes(GetReplyAttributesRe result[attribute.Key] = values; } - // Если атрибут достаточный - прекращаем обработку if (IsSufficientAttribute(attribute.Value)) { _logger.LogDebug("Sufficient attribute '{Attribute}' found, stopping processing", attribute.Key); @@ -51,7 +49,7 @@ public IDictionary> GetReplyAttributes(GetReplyAttributesRe private List ProcessAttribute( string attributeName, - RadiusReplyAttribute[] attributeValues, + IRadiusReplyAttribute[] attributeValues, GetReplyAttributesRequest request) { var result = new List(); @@ -86,9 +84,8 @@ private List ProcessAttribute( return result; } - private bool ShouldIncludeAttribute(RadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + private bool ShouldIncludeAttribute(IRadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) { - // 1. Проверка LDAP атрибутов if (attributeValue.FromLdap) { if (attributeValue.IsMemberOf) @@ -98,19 +95,16 @@ private bool ShouldIncludeAttribute(RadiusReplyAttribute attributeValue, GetRepl request.HasAttribute(attributeValue.Name); } - // 2. Проверка условий по имени пользователя if (attributeValue.UserNameCondition.Count > 0) { return MatchesUserNameCondition(attributeValue.UserNameCondition, request.UserName); } - // 3. Проверка условий по группам if (attributeValue.UserGroupCondition.Count > 0) { return MatchesUserGroupCondition(attributeValue.UserGroupCondition, request.UserGroups); } - // 4. Без условий - всегда включаем return true; } @@ -144,7 +138,7 @@ private static bool MatchesUserGroupCondition(IReadOnlyList conditions, .Any(group => string.Equals(group, condition, StringComparison.OrdinalIgnoreCase))); } - private static List GetAttributeValues(RadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) + private static List GetAttributeValues(IRadiusReplyAttribute attributeValue, GetReplyAttributesRequest request) { if (attributeValue.IsMemberOf) { @@ -163,7 +157,7 @@ private static bool MatchesUserGroupCondition(IReadOnlyList conditions, return [attributeValue.Value]; } - private static bool IsSufficientAttribute(RadiusReplyAttribute[] attributeValues) + private static bool IsSufficientAttribute(IRadiusReplyAttribute[] attributeValues) { return attributeValues.Any(av => av.Sufficient); } diff --git a/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs b/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs deleted file mode 100644 index 7999d6b2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Shared/Attributes/ConfigAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Multifactor.Radius.Adapter.v2.Shared.Attributes; - -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public class ConfigParameterAttribute : Attribute -{ - public string XmlName { get; } - public string EnvName { get; } - public object? DefaultValue { get; } - public bool Required { get; } - - public ConfigParameterAttribute(string xmlName, object? defaultValue = null) - { - XmlName = xmlName; - EnvName = ConvertToEnvName(xmlName); - DefaultValue = defaultValue; - } - - private static string ConvertToEnvName(string xmlName) - { - return xmlName.Replace('-', '_').ToUpperInvariant(); - } -} - -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -public class ComplexConfigParameterAttribute : Attribute -{ - public string XmlElementName { get; } - - public ComplexConfigParameterAttribute(string xmlElementName) - { - XmlElementName = xmlElementName; - } -} \ No newline at end of file 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 bf0c1b88..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChallengeProcessorProviderTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; - -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 2c4172a6..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/ChangePasswordChallengeProcessorTests.cs +++ /dev/null @@ -1,366 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using Multifactor.Radius.Adapter.v2.Services.DataProtection; -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 f7c3a0bf..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/AccessChallengeTests/SecondFactorChallengeProcessorTests.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models.Enums; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -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..a0994aff --- /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.Configuration.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..b7472fd7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs @@ -0,0 +1,253 @@ +using System.DirectoryServices.Protocols; +using System.Net; +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Schema; +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.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..e4014e22 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs @@ -0,0 +1,92 @@ +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.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..2c504649 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs @@ -0,0 +1,257 @@ +using System.Net; +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.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 _radiusClientFactoryMock; + private readonly Mock> _loggerMock; + private readonly Mock _radiusClientMock; + private readonly RadiusFirstFactorProcessor _processor; + + public RadiusFirstFactorProcessorTests() + { + _radiusPacketServiceMock = new Mock(); + _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 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_ShouldAcceptWhenRadiusAccepts() + { + // Arrange + var requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + 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 context = CreateContext(requestPacket); + + 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 requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + context.ClientConfiguration.NpsServerEndpoints = new[] + { + new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812), + new IPEndPoint(IPAddress.Parse("192.168.1.2"), 1812) + }; + + 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 requestPacket = CreateRadiusPacket("testuser"); + var context = CreateContext(requestPacket); + + context.ClientConfiguration.NpsServerEndpoints = new[] + { + new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812), + new IPEndPoint(IPAddress.Parse("192.168.1.2"), 1812) + }; + + _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 RadiusPipelineContext CreateContext(RadiusPacket requestPacket) + { + var clientConfig = new ClientConfiguration + { + Name = "TestClient", + RadiusSharedSecret = "shared-secret", + AdapterClientEndpoint = new IPEndPoint(IPAddress.Any, 0), + NpsServerEndpoints = new[] { new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812) }, + NpsServerTimeout = TimeSpan.FromSeconds(5) + }; + + 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..b258fd33 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs @@ -0,0 +1,114 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +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..77606326 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs @@ -0,0 +1,143 @@ +using System.Net; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; +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..23361e1c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs @@ -0,0 +1,267 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Radius.Adapter.v2.Application.Configuration.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.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..e9f9f318 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs @@ -0,0 +1,126 @@ +using System.Net; +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.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..9d796183 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs @@ -0,0 +1,302 @@ +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.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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + 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 context = CreateTestContext(); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.Ldap; + + 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 context = CreateTestContext(); + context.FirstFactorStatus = AuthenticationStatus.Awaiting; + context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.None; + + 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 RadiusPipelineContext CreateTestContext() + { + var clientConfig = new ClientConfiguration + { + Name = "test-client", + RadiusSharedSecret = "test-secret", + FirstFactorAuthenticationSource = AuthenticationSource.Radius + }; + + var requestPacket = new RadiusPacket( + new RadiusPacketHeader(PacketCode.AccessRequest, 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/IpWhiteListStepTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs new file mode 100644 index 00000000..8ac3efb7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs @@ -0,0 +1,152 @@ +using System.Net; +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; +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..649bc762 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs @@ -0,0 +1,286 @@ +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.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..1201cf44 --- /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.Configuration.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..dfc3dab5 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs @@ -0,0 +1,188 @@ +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 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..76ba2e36 --- /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..4806b828 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs @@ -0,0 +1,327 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Multifactor.Core.Ldap.Name; +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.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); + context.LdapProfile = ldapProfileMock.Object; + context.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..c6bd7bce --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs @@ -0,0 +1,371 @@ +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.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 6f3a3dec..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ClientConfigurationFactoryTests/AppSettingsTests.cs +++ /dev/null @@ -1,939 +0,0 @@ -using System.Net; -using Moq; -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 3cbde24f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/LdapServerConfigurationTests.cs +++ /dev/null @@ -1,257 +0,0 @@ -using Multifactor.Core.Ldap.Name; -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 7b800669..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ConfigurationTests/ServiceConfigurationFactoryTests.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -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 793d8afa..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/CustomLdapSchemaLoaderTests.cs +++ /dev/null @@ -1,38 +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.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 4890f286..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -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 0702d37b..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Infrastructure.Radius; -using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; -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 d887bc7e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Fixture/TestUtils.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; -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 cad06719..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapPasswordChangerTests.cs +++ /dev/null @@ -1,99 +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.Application.Ports.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 1a42974f..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileLoaderTests.cs +++ /dev/null @@ -1,42 +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.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 3c966cfc..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/LdapProfile/LdapProfileServiceTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap.Models; -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 22358b84..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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 Application.Features.Ldap.Models.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 79a2ceef..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/BuildPipelineTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; - -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 28d80037..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PerformanceTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Diagnostics; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; -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 12c2f385..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineConfigurationFactoryTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps.Challenge; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; -using Multifactor.Radius.Adapter.v2.Services.Cache; -using AccessChallengeStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.AccessChallengeStep; -using IpWhiteListStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.IpWhiteListStep; -using SecondFactorStep = Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps.SecondFactorStep; - -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 61c815a3..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/PipelineExecutionTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; - -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 0718c0a2..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessGroupsCheckingStepTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Name; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -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 07f649df..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/AccessRequestFilteringStepTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; - -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 4558e08e..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthCheckStepTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -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 ea88f87a..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/PreAuthPostCheckStepTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; - -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 b8098e02..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/ProfileLoadingStepTests.cs +++ /dev/null @@ -1,166 +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.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Services.Cache; -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 f71975b7..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/SecondFactorStepTests.cs +++ /dev/null @@ -1,451 +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.Application.Features.AccessChallenge; -using Multifactor.Radius.Adapter.v2.Application.Features.AccessChallenge.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; - -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 0288cb9a..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/StatusServerFilteringStepTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.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 257ddc61..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/PipelineTests/StepsTests/UserGroupLoadingStepTests.cs +++ /dev/null @@ -1,276 +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.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -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 d32f36af..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/NasIdentifierParserTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Infrastructure.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 097ffae9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/AttributeReadingTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Infrastructure.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 d8c9b1c4..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/RadiusPacketParsingTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Infrastructure.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 b01c386c..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/PacketService/ResponsePacketTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Multifactor.Radius.Adapter.v2.Infrastructure.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 6c2dabac..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusAttributeTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -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 ca9a47dc..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Radius/RadiusPacketTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -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 132e259b..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Server/UdpPacketHandlerTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Services; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; -using Multifactor.Radius.Adapter.v2.Server; -using Multifactor.Radius.Adapter.v2.Services.Cache; -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 d92334c9..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/ServiceCollectionExtensionsTests/AddPipelineTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; -using Multifactor.Radius.Adapter.v2.Application.Models; -using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Extensions; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.XmlAppConfiguration; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Configuration; - -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 08a7b6c1..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/AdapterResponseSenderTests.cs +++ /dev/null @@ -1,421 +0,0 @@ -# nullable disable -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Ports; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.RandomWaiterFeature; -using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; - -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 8d98f2eb..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs +++ /dev/null @@ -1,119 +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.Application.Features.FirstFactor; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -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 4f967558..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuthTests/RadiusFirstFactorProcessorTests.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.FirstFactor; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Ports; -using Multifactor.Radius.Adapter.v2.Application.Ports; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; - -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 f0e92632..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.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Ports.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 4f21fa88..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.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Ports.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 6e8ab786..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.Application.Features.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 864df9a0..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.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Ports.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 5638deed..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.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Ports.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 5344268b..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.Application.Features.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Application.Ports.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/LdapGroupServiceTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs deleted file mode 100644 index b416e4e5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/LdapGroupServiceTests.cs +++ /dev/null @@ -1,292 +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.Application.Features.Pipeline; -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 a28c4178..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiServiceTests.cs +++ /dev/null @@ -1,1360 +0,0 @@ -# nullable disable -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.MultifactorApi; -using Multifactor.Radius.Adapter.v2.Application.Ports.Ldap; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; -using Multifactor.Radius.Adapter.v2.Exceptions; -using Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; -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 62927f67..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MultifactorApi/MultifactorApiTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -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 d76c7bd5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineSteps/IpWhiteListStepTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Features.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 f95da5aa..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/PipelineTests/StepsTests/UserNameValidationStepTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline; -using Multifactor.Radius.Adapter.v2.Application.Pipeline.Steps; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -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 05a838ec..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.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; - -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 3c871add..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/RadiusReplyAttributeServiceTests.cs +++ /dev/null @@ -1,262 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Core.Ldap.Attributes; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; -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 cb31bc12..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/UserIdentityTests/UserIdentityTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models.Enum; -using Multifactor.Radius.Adapter.v2.Infrastructure.Adapters.Ldap.Models; -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/App.config b/src/Multifactor.Radius.Adapter.v2/App.config index a8434d70..9a3cb0a3 100644 --- a/src/Multifactor.Radius.Adapter.v2/App.config +++ b/src/Multifactor.Radius.Adapter.v2/App.config @@ -7,21 +7,21 @@ - + - + - + - - + + - - + - @@ -29,11 +29,11 @@ - + - + diff --git a/src/Multifactor.Radius.Adapter.v2/Dockerfile b/src/Multifactor.Radius.Adapter.v2/Dockerfile index b53d4248..60f7c357 100644 --- a/src/Multifactor.Radius.Adapter.v2/Dockerfile +++ b/src/Multifactor.Radius.Adapter.v2/Dockerfile @@ -17,20 +17,30 @@ FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "RadiusV2.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false -# Начинаем финальную стадию FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final -USER app WORKDIR /app EXPOSE 8080 EXPOSE 8081 -# Устанавливаем OpenLDAP и создаем симлинк +USER root + +# Устанавливаем библиотеки LDAP RUN apt-get update && \ - apt-get install -y libldap-2.5-0 && \ - ln -s /usr/lib/x86_64-linux-gnu/libldap-2.5.so.0 /usr/lib/x86_64-linux-gnu/libldap-2.4.so.2 && \ + apt-get install -y \ + libldap-2.4-2 \ + libldap-common \ + libsasl2-2 \ + libsasl2-modules && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -# Копируем опубликованное приложение из стадии publish +# Создаем симлинк в директории .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", "RadiusV2.dll"] \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs index d879fc9e..d7554f8b 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -18,7 +18,7 @@ public class AdapterServer : IAsyncDisposable private readonly SemaphoreSlim _concurrencyLimiter; private readonly ConcurrentBag _activeProcessingTasks = []; - //TODO Возможно сразу в конфигурацию вынести + //TODO to the configuration private const int ShoutDownTimeout = 30; private const int MaxConcurrentRequests = 1000; diff --git a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs index 0a1d696e..4de16010 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs @@ -17,7 +17,7 @@ public ServerHost(AdapterServer server, ILogger logger) _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken = default) { _logger.LogInformation("Starting RADIUS server host..."); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -35,7 +35,7 @@ public async Task StartAsync(CancellationToken cancellationToken) } } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken = default) { _logger.LogInformation("Stopping RADIUS server host..."); try diff --git a/src/multifactor-radius-adapter.sln b/src/multifactor-radius-adapter.sln index 56300b13..b096dada 100644 --- a/src/multifactor-radius-adapter.sln +++ b/src/multifactor-radius-adapter.sln @@ -9,6 +9,7 @@ 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}" @@ -19,8 +20,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multifactor.Radius.Adapter. 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}" -EndProject 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}" @@ -49,10 +48,6 @@ 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 From 3ec4959b4a959539157b78e4f6a0a5e4b19cbab2 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 5 Feb 2026 11:25:51 +0300 Subject: [PATCH 07/11] return radius v1 --- .../ChallengeProcessorProviderTests.cs | 1 + .../ChangePasswordChallengeProcessorTests.cs | 2 + .../Fixtures/TestHostFactory.cs | 2 +- .../LdapIdentityTests.cs | 3 +- .../LdapUserTests.cs | 1 + .../PreAuthModeSettingsTests.cs | 2 +- .../RadiusReplyAttributeRewriterTests.cs | 4 +- .../Core/Framework/Context/UserPassphrase.cs | 123 ++++++++++++++++++ .../Attributes/DictionaryVendorAttribute.cs | 48 +++++++ .../Attributes/VendorSpecificAttribute.cs | 64 +++++++++ .../Core/Radius/SharedSecret.cs | 61 +++++++++ .../Http/BasicAuthHeaderValue.cs | 84 ++++++++++++ .../Services/Ldap/PasswordChangeRequest.cs | 14 ++ .../MultifactorApiUnreachableException.cs | 20 +++ 14 files changed, 425 insertions(+), 4 deletions(-) create mode 100644 src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs create mode 100644 src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs create mode 100644 src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs create mode 100644 src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs create mode 100644 src/MultiFactor.Radius.Adapter/Infrastructure/Http/BasicAuthHeaderValue.cs create mode 100644 src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs create mode 100644 src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs diff --git a/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs b/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs index 825c3cb5..1e28a12a 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/ChallengeProcessorProviderTests.cs @@ -1,4 +1,5 @@ using System.Net; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs b/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs index f68c991a..e63ec2c4 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/ChangePasswordChallengeProcessorTests.cs @@ -1,8 +1,10 @@ using System.Net; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; +using MultiFactor.Radius.Adapter.Core; using MultiFactor.Radius.Adapter.Core.Framework.Context; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ClientLevel; diff --git a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs index 4a8b2094..615f3e06 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/Fixtures/TestHostFactory.cs @@ -8,9 +8,9 @@ using MultiFactor.Radius.Adapter.Extensions; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ConfigurationLoading; +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Core; using MultiFactor.Radius.Adapter.Tests.Fixtures.ConfigLoading; using System.Net; -using MultiFactor.Radius.Adapter.Server; namespace MultiFactor.Radius.Adapter.Tests.Fixtures; diff --git a/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs b/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs index 1c7cd524..614ed849 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/LdapIdentityTests.cs @@ -1,4 +1,5 @@ -using MultiFactor.Radius.Adapter.Services.Ldap; +using MultiFactor.Radius.Adapter.Core.Services.Ldap; +using MultiFactor.Radius.Adapter.Services.Ldap; namespace MultiFactor.Radius.Adapter.Tests { diff --git a/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs b/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs index 19cd6dbd..70da6c8c 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/LdapUserTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using MultiFactor.Radius.Adapter.Core.Services.Ldap; using MultiFactor.Radius.Adapter.Services.Ldap; namespace MultiFactor.Radius.Adapter.Tests diff --git a/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs b/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs index 9d526bb1..44bd5233 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/PreAuthModeSettingsTests.cs @@ -1,6 +1,6 @@ using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Features.PreAuthModeFeature; -namespace MultiFactor.Radius.Adapter.Tests; +namespace MultiFactor.Radius.Adapter.Tests.AdapterConfig; public partial class ConfigurationLoadingTests { diff --git a/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs b/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs index f86777e8..723ddc31 100644 --- a/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs +++ b/src/MultiFactor.Radius.Adapter.Tests/RadiusReplyAttributeRewriterTests.cs @@ -1,6 +1,8 @@ -using FluentAssertions; +using System.Net; +using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Moq; +using MultiFactor.Radius.Adapter.Core.Framework.Context; using MultiFactor.Radius.Adapter.Core.Radius.Attributes; using MultiFactor.Radius.Adapter.Infrastructure.Configuration; using MultiFactor.Radius.Adapter.Infrastructure.Configuration.ClientLevel; diff --git a/src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs b/src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs new file mode 100644 index 00000000..ee1c8b89 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Core/Framework/Context/UserPassphrase.cs @@ -0,0 +1,123 @@ +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md + +using MultiFactor.Radius.Adapter.Infrastructure.Configuration.Features.PreAuthModeFeature; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace MultiFactor.Radius.Adapter.Core.Framework.Context +{ + public class UserPassphrase + { + private static readonly string[] _providerCodes = { "t", "m", "s", "c" }; + + /// + /// User-Password attribute raw value. + /// + public string Raw { get; } + + /// + /// User password. + /// + public string Password { get; } + + /// + /// 6 digits. + /// + public string Otp { get; } + + /// + /// Maybe one of 't', 'm', 's' or 'c'.
+ /// t: Telegram
+ /// m: MobileApp
+ /// s: SMS
+ /// 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; } + + /// + /// 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) + { + Raw = raw; + Password = password; + Otp = otp; + ProviderCode = providerCode; + } + + public static UserPassphrase Parse(string rawPwd, PreAuthModeDescriptor preAuthnMode) + { + if (preAuthnMode is null) + { + throw new ArgumentNullException(nameof(preAuthnMode)); + } + + var hasOtp = TryGetOtpCode(rawPwd, preAuthnMode, out var otp); + if (!hasOtp) + { + otp = null; + } + + var pwd = GetPassword(rawPwd, preAuthnMode, hasOtp); + if (string.IsNullOrEmpty(pwd)) + { + pwd = null; + } + + 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) + { + var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; + switch (preAuthnMode.Mode) + { + case PreAuthMode.Otp: + var length = preAuthnMode.Settings.OtpCodeLength; + if (passwordAndOtp.Length < length) + { + return passwordAndOtp; + } + + if (!hasOtp) + { + return passwordAndOtp; + } + + var sub = passwordAndOtp[..^length]; + return sub; + + case PreAuthMode.None: + default: + return passwordAndOtp; + } + } + + private static bool TryGetOtpCode(string rawPwd, PreAuthModeDescriptor preAuthnMode, out string code) + { + var passwordAndOtp = rawPwd?.Trim() ?? string.Empty; + var length = preAuthnMode.Settings.OtpCodeLength; + if (passwordAndOtp.Length < length) + { + code = null; + return false; + } + + code = passwordAndOtp[^length..]; + if (!Regex.IsMatch(code, preAuthnMode.Settings.OtpCodeRegex)) + { + code = null; + return false; + } + + return true; + } + } +} diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs new file mode 100644 index 00000000..ef30a1b9 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/DictionaryVendorAttribute.cs @@ -0,0 +1,48 @@ +//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; + +namespace MultiFactor.Radius.Adapter.Core.Radius.Attributes +{ + public class DictionaryVendorAttribute : DictionaryAttribute + { + public readonly uint VendorId; + public readonly uint VendorCode; + + + /// + /// Create a dictionary vendor specific attribute + /// + /// + /// + /// + /// + public DictionaryVendorAttribute(uint vendorId, string name, uint vendorCode, string type) : base(name, 26, type) + { + VendorId = vendorId; + VendorCode = vendorCode; + } + } +} diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs new file mode 100644 index 00000000..8d929a37 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Core/Radius/Attributes/VendorSpecificAttribute.cs @@ -0,0 +1,64 @@ +//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; + +namespace MultiFactor.Radius.Adapter.Core.Radius.Attributes +{ + public class VendorSpecificAttribute + { + public byte Length; + public uint VendorId; + public byte VendorCode; + public Type VendorType; + public byte[] Value; + + + /// + /// Create a vsa from bytes + /// + /// + public VendorSpecificAttribute(byte[] contentBytes) + { + var vendorId = new byte[4]; + Buffer.BlockCopy(contentBytes, 0, vendorId, 0, 4); + Array.Reverse(vendorId); + VendorId = BitConverter.ToUInt32(vendorId, 0); + + var vendorType = new byte[1]; + Buffer.BlockCopy(contentBytes, 4, vendorType, 0, 1); + VendorCode = vendorType[0]; + + var vendorLength = new byte[1]; + Buffer.BlockCopy(contentBytes, 5, vendorLength, 0, 1); + Length = vendorLength[0]; + + var value = new byte[contentBytes.Length - 6]; + Buffer.BlockCopy(contentBytes, 6, value, 0, contentBytes.Length - 6); + Value = value; + } + } +} diff --git a/src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs b/src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs new file mode 100644 index 00000000..cafeca0d --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Core/Radius/SharedSecret.cs @@ -0,0 +1,61 @@ +//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; +using System.Text; + +namespace MultiFactor.Radius.Adapter.Core.Radius +{ + 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/Infrastructure/Http/BasicAuthHeaderValue.cs b/src/MultiFactor.Radius.Adapter/Infrastructure/Http/BasicAuthHeaderValue.cs new file mode 100644 index 00000000..10f9380a --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Infrastructure/Http/BasicAuthHeaderValue.cs @@ -0,0 +1,84 @@ +using System; +using System.Text; + +namespace MultiFactor.Radius.Adapter.Infrastructure.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/Services/Ldap/PasswordChangeRequest.cs b/src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs new file mode 100644 index 00000000..91125d60 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Services/Ldap/PasswordChangeRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace MultiFactor.Radius.Adapter.Services.Ldap; + +public class PasswordChangeRequest +{ + 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/Services/MultiFactorApi/MultifactorApiUnreachableException.cs b/src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs new file mode 100644 index 00000000..d26961f6 --- /dev/null +++ b/src/MultiFactor.Radius.Adapter/Services/MultiFactorApi/MultifactorApiUnreachableException.cs @@ -0,0 +1,20 @@ +//Copyright(c) 2020 MultiFactor +//Please see licence at +//https://github.com/MultifactorLab/multifactor-radius-adapter/blob/main/LICENSE.md + + +using System; + +namespace MultiFactor.Radius.Adapter.Services.MultiFactorApi +{ + [Serializable] + internal 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) { } + } +} From f1401ca422611894f2991469c18ccf723c18bf02 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 5 Feb 2026 11:48:15 +0300 Subject: [PATCH 08/11] remove radius V1 --- src/Multifactor.Radius.Adapter.v2/App.config | 35 ++++---------------- src/multifactor-radius-adapter.sln | 12 ------- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/src/Multifactor.Radius.Adapter.v2/App.config b/src/Multifactor.Radius.Adapter.v2/App.config index 9a3cb0a3..949abf36 100644 --- a/src/Multifactor.Radius.Adapter.v2/App.config +++ b/src/Multifactor.Radius.Adapter.v2/App.config @@ -7,33 +7,18 @@ - + - - + + - + - + - - - - - - - - - - - - - - - - - + @@ -43,13 +28,5 @@ - - - - - - - - \ No newline at end of file diff --git a/src/multifactor-radius-adapter.sln b/src/multifactor-radius-adapter.sln index b096dada..3b36bd2c 100644 --- a/src/multifactor-radius-adapter.sln +++ b/src/multifactor-radius-adapter.sln @@ -12,10 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution 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}" @@ -32,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 From 10ad7dca1e90ab11d51230690b77d2d3f3125de2 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Mon, 9 Feb 2026 09:02:50 +0300 Subject: [PATCH 09/11] Fix configuration loader --- .../InvalidConfigurationException.cs | 6 --- .../Loader/ConfigurationLoader.cs | 3 +- .../Models/ClientConfiguration.cs | 24 ++++++----- .../Models/ConfigurationFile.cs | 9 ++++- .../Models/LdapServerConfiguration.cs | 11 +++-- .../Models/RootConfiguration.cs | 40 +++++++++---------- 6 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs index 0a15ad06..d8c49fa1 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -39,12 +39,6 @@ public static InvalidConfigurationException For(Expression(Expression> propertySelector, string filePath) - { - const string message = "Property '{prop}' is required. Config name: '{1}'"; - return For(c => propertySelector, message, filePath); ; - } private static string Property(Expression> propertySelector) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs index ec9c5f5b..25afa85c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -80,8 +80,7 @@ private ClientConfiguration ParseClientConfiguration(string filePath) var clientConfig = ClientConfiguration.FromConfiguration(config); clientConfig.ReplyAttributes = ParseReplyAttributes(config.RadiusReply); - clientConfig.LdapServers = config.LdapServers.Select(LdapServerConfiguration.FromConfiguration).ToList(); - + clientConfig.LdapServers = config.LdapServers.Select(conf => LdapServerConfiguration.FromConfiguration(conf, clientConfig.Name)).ToList(); return clientConfig; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs index f447030a..11ba3966 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs @@ -42,16 +42,16 @@ public static ClientConfiguration FromConfiguration(ConfigurationFile configurat var dto = new ClientConfiguration { Name = configurationFile.FileName, - MultifactorNasIdentifier = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorNasIdentifier) ? configurationFile.AppSettings.MultifactorNasIdentifier - : throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.MultifactorNasIdentifier, configurationFile.FileName), - MultifactorSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.MultifactorSharedSecret) ? configurationFile.AppSettings.MultifactorNasIdentifier - : throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.MultifactorSharedSecret, 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.MultifactorNasIdentifier : + throw InvalidConfigurationException.For(prop => prop.AppSettings.MultifactorSharedSecret, "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName), SignUpGroups = ConfigurationValueProcessor.TryParseStringList(configurationFile.AppSettings.SignUpGroups, out var list) ? list : [], BypassSecondFactorWhenApiUnreachable = configurationFile.AppSettings.BypassSecondFactorWhenApiUnreachable, RadiusClientIp = ConfigurationValueProcessor.TryParseIpAddress(configurationFile.AppSettings.RadiusClientIp, out var address) ? address : null, RadiusClientNasIdentifier = configurationFile.AppSettings.RadiusClientNasIdentifier, RadiusSharedSecret = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.RadiusSharedSecret) ? configurationFile.AppSettings.RadiusSharedSecret - : throw InvalidConfigurationException.For(c => c.AppSettings.RadiusSharedSecret, "Property '{prop}' is required. Config name: '{1}'", configurationFile.FileName), + : throw InvalidConfigurationException.For(c => c.AppSettings.RadiusSharedSecret, "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName), NpsServerEndpoints = ConfigurationValueProcessor.TryParseEndpoints(configurationFile.AppSettings.NpsServerEndpoints, out var npsServerEndpoints) ? npsServerEndpoints : [], NpsServerTimeout = ConfigurationValueProcessor.TryParseTimeout(configurationFile.AppSettings.NpsServerTimeout, out var timeout) ? timeout.Value : TimeSpan.Parse("00:00:05"), @@ -65,18 +65,22 @@ public static ClientConfiguration FromConfiguration(ConfigurationFile configurat : null }; - var firstFactorAuthenticationSource = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.FirstFactorAuthenticationSource) ? configurationFile.AppSettings.FirstFactorAuthenticationSource : - throw InvalidConfigurationException.RequiredFor(c => c.AppSettings.FirstFactorAuthenticationSource, configurationFile.FileName); + 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 = ConfigurationValueProcessor.TryParseEnum(firstFactorAuthenticationSource, out var source) ? source - : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{1}'", configurationFile.FileName); ; + : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{0}'", configurationFile.FileName); ; var adapterClientEndpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint) ? configurationFile.AppSettings.AdapterClientEndpoint : - throw InvalidConfigurationException.For(c => c.AppSettings.AdapterClientEndpoint, "Property '{prop}' is required. Config name: '{1}'", configurationFile.FileName); + throw InvalidConfigurationException.For(c => c.AppSettings.AdapterClientEndpoint, "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName); dto.AdapterClientEndpoint = ConfigurationValueProcessor.TryParseEndpoint(adapterClientEndpoint, out var endpoint) ? endpoint! : - throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{1}'", configurationFile.FileName); ; + throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{0}'", configurationFile.FileName); ; return dto; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs index 019e4104..497bf5fc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs @@ -6,11 +6,16 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; public class ConfigurationFile { - public ConfigurationFile() { } + public ConfigurationFile() + { + 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; } = new(); + public List LdapServers { get; set; } = []; public RadiusReplySection RadiusReply { get; set; } = new(); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs index a1078c18..d985bc7d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs @@ -28,16 +28,19 @@ public class LdapServerConfiguration : ILdapServerConfiguration public IReadOnlyList ExcludedSuffixes { get; init; } public IReadOnlyList BypassSecondFactorWhenApiUnreachableGroups { get; init; } - public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapServerSection) + public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapServerSection, string fileName) { var dto = new LdapServerConfiguration { ConnectionString = !string.IsNullOrWhiteSpace(ldapServerSection.ConnectionString) ? ldapServerSection.ConnectionString : - throw InvalidConfigurationException.RequiredFor(section => section.LdapServers[0].ConnectionString, nameof(ldapServerSection)), + 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.RequiredFor(section => section.LdapServers[0].Username, nameof(ldapServerSection)), + 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.RequiredFor(section => section.LdapServers[0].Password, nameof(ldapServerSection)), + throw InvalidConfigurationException.For(prop => prop.LdapServers[0].Password, "Property '{prop}' is required. Config name: '{0}'", fileName), + BindTimeoutSeconds = ldapServerSection.BindTimeoutSeconds ?? 30, AccessGroups = ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.AccessGroups, diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs index 09eabd7f..d0f6294a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs @@ -30,29 +30,29 @@ public static RootConfiguration FromConfiguration(ConfigurationFile configuratio ArgumentNullException.ThrowIfNull(configurationFile); var conf = new RootConfiguration { - MultifactorApiProxy = configurationFile.AppSettings.MultifactorApiProxy, + MultifactorApiProxy = configurationFile.AppSettings?.MultifactorApiProxy, MultifactorApiTimeout = ConfigurationValueProcessor.TryParseTimeout( - configurationFile.AppSettings.MultifactorApiTimeout, out var span) + configurationFile.AppSettings?.MultifactorApiTimeout, out var span) ? span!.Value : TimeSpan.FromSeconds(65), - LoggingFormat = configurationFile.AppSettings.LoggingFormat, - SyslogUseTls = configurationFile.AppSettings.SyslogUseTls, - 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" + 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.RequiredFor(prop => prop.AppSettings.MultifactorApiUrl, configurationFile.FileName); - conf.MultifactorApiUrls = ConfigurationValueProcessor.TryParseUrls(urls, out var parsedUrls) - ? parsedUrls - : throw new InvalidConfigurationException($"Invalid 'multifactor-api-url': '{urls}'", configurationFile.FileName); - var endpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterServerEndpoint) ? configurationFile.AppSettings.AdapterServerEndpoint : throw new InvalidConfigurationException(nameof(conf.AdapterServerEndpoint)); + 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 = ConfigurationValueProcessor.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 = ConfigurationValueProcessor.TryParseEndpoint(endpoint, out var point) ? point From f25b32554541aeca49ab89066f6f0a806ff996c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Tue, 10 Feb 2026 01:17:06 +0300 Subject: [PATCH 10/11] Review fixes --- .../Models/IClientConfiguration.cs | 46 +++-- .../Models/ILdapServerConfiguration.cs | 40 ++-- .../Models/IRadiusReplyAttribute.cs | 10 +- .../Models/IRootConfiguration.cs | 33 ++- .../Models/ServiceConfiguration.cs | 6 +- .../Models/GetReplyAttributesRequest.cs | 4 +- .../Models/SendAdapterResponseRequest.cs | 4 +- .../Adapters/Ldap/LdapAdapter.cs | 4 +- .../InvalidConfigurationException.cs | 6 +- .../Loader/ConfigurationLoader.cs | 13 +- .../Models/ClientConfiguration.cs | 45 +++-- .../Models/ConfigurationFile.cs | 13 +- .../Models/LdapServerConfiguration.cs | 24 +-- .../Models/RootConfiguration.cs | 10 +- ...ocessor.cs => ConfigurationValueParser.cs} | 19 +- .../Reader/ConfigurationBuilderExtensions.cs | 4 +- .../Reader/ConfigurationReader.cs | 15 +- .../Extensions/InfrastructureExtensions.cs | 1 - .../Logging/SerilogLoggerFactory.cs | 17 +- .../Logging/StartupLogger.cs | 26 +-- .../Builders/IRadiusAttributeSerializer.cs | 8 - .../Builders/RadiusAttributeSerializer.cs | 190 ------------------ .../Radius/Builders/RadiusPacketBuilder.cs | 8 +- .../Radius/Parsers/RadiusAttributeParser.cs | 2 +- .../Radius/Sender/AdapterResponseSender.cs | 6 +- .../Services/RadiusReplyAttributeService.cs | 4 +- .../RadiusFirstFactorProcessorTests.cs | 46 +++-- .../Pipeline/Steps/FirstFactorStepTests.cs | 41 ++-- .../Pipeline/Steps/ProfileLoadingStepTests.cs | 4 +- .../Pipeline/Steps/SecondFactorStepTests.cs | 10 +- src/Multifactor.Radius.Adapter.v2/Program.cs | 10 +- .../Server/AdapterServer.cs | 13 +- .../Server/ServerHost.cs | 5 +- 33 files changed, 253 insertions(+), 434 deletions(-) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/{ConfigurationValueProcessor.cs => ConfigurationValueParser.cs} (94%) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs delete mode 100644 src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs index 60fe8e57..8de550f5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IClientConfiguration.cs @@ -6,29 +6,33 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public interface IClientConfiguration { - public string Name { get; set; } + public string Name { get; } - 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 string MultifactorNasIdentifier { get; } + public string MultifactorSharedSecret { get; } + public IReadOnlyList SignUpGroups { get; } + public bool BypassSecondFactorWhenApiUnreachable { get; } + public AuthenticationSource FirstFactorAuthenticationSource { get; } + public IPEndPoint AdapterClientEndpoint { get; } - public IPAddress? RadiusClientIp { get; set; } - public string RadiusClientNasIdentifier { get; set; } - public string RadiusSharedSecret { get; set; } - public IPEndPoint[] NpsServerEndpoints { get; set; } - public TimeSpan NpsServerTimeout { get; set; } + public IPAddress? RadiusClientIp { get; } + public string RadiusClientNasIdentifier { get; } + public string RadiusSharedSecret { get; } + public IReadOnlyList NpsServerEndpoints { get; } + public TimeSpan NpsServerTimeout { get; } - public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } + public Privacy Privacy { get; } - public PreAuthMode? PreAuthenticationMethod { get; set; } - public TimeSpan AuthenticationCacheLifetime { get; set; } - public (int min, int max)? InvalidCredentialDelay { get; set; } - public string? CallingStationIdAttribute { get; set; } //TODO not used - public IReadOnlyList IpWhiteList { get; set; } + 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; set; } - public IReadOnlyDictionary? ReplyAttributes { get; set; } -} \ No newline at end of file + public IReadOnlyList? LdapServers { get; } + public IReadOnlyDictionary>? ReplyAttributes { get; } +} + +public record Privacy(PrivacyMode PrivacyMode, string[] PrivacyFields); + +public record CredentialDelay(int Min, int Max); \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs index b28c60c6..b80df211 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ILdapServerConfiguration.cs @@ -4,24 +4,24 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public interface 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; } - public bool TrustedDomainsEnabled { get; init; } - public bool AlternativeSuffixesEnabled { get; init; } - 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 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 index a623369c..dac3b8f9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRadiusReplyAttribute.cs @@ -2,11 +2,11 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public interface IRadiusReplyAttribute { - public string Name { get; set; } - public object Value { get; set; } - public IReadOnlyList UserGroupCondition { get; set; } - public IReadOnlyList UserNameCondition { get; set; } - public bool Sufficient { get; set; } + 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 index 362401c8..a2aaee90 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/IRootConfiguration.cs @@ -4,22 +4,21 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public interface IRootConfiguration { - - IReadOnlyList MultifactorApiUrls { get; set; } - string? MultifactorApiProxy { get; set; } - TimeSpan MultifactorApiTimeout { get; set; } - IPEndPoint? AdapterServerEndpoint { get; set; } - string LoggingLevel { get; set; } - string? LoggingFormat { get; set; } - bool SyslogUseTls { get; set; } - string? SyslogServer { get; set; } - string? SyslogFormat { get; set; } - string? SyslogFacility { get; set; } - string SyslogAppName { get; set; } - string? SyslogFramer { get; set; } - string? SyslogOutputTemplate { get; set; } + 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; set; } - string? FileLogOutputTemplate { get; set; } - int LogFileMaxSizeBytes { get; set; } + 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 index a0f121bd..48597f58 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Configuration/Models/ServiceConfiguration.cs @@ -4,8 +4,8 @@ namespace Multifactor.Radius.Adapter.v2.Application.Configuration.Models; public class ServiceConfiguration { - public required IRootConfiguration RootConfiguration { get; set; } - public required IReadOnlyList ClientsConfigurations { get; set; } + 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) { @@ -18,5 +18,5 @@ public class ServiceConfiguration config.RadiusClientIp != null && config.RadiusClientIp.Equals(ip)); } - public bool SingleClientMode { get; set; } + public bool SingleClientMode { get; init; } } \ No newline at end of file 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 index 20a1cfe6..06efc820 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/GetReplyAttributesRequest.cs @@ -8,13 +8,13 @@ public class GetReplyAttributesRequest { public string? UserName { get; } public HashSet UserGroups { get; } - public IReadOnlyDictionary ReplyAttributes { get; } + public IReadOnlyDictionary> ReplyAttributes { get; } private IReadOnlyCollection Attributes { get; } public GetReplyAttributesRequest( string? userName, HashSet userGroups, - IReadOnlyDictionary replyAttributes, + IReadOnlyDictionary> replyAttributes, IReadOnlyCollection userAttributes) { ArgumentNullException.ThrowIfNull(userGroups); 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 index e4ef0e35..2c1442d2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Radius/Models/SendAdapterResponseRequest.cs @@ -19,9 +19,9 @@ public class SendAdapterResponseRequest public ResponseInformation ResponseInformation { get; set; } public SharedSecret RadiusSharedSecret { get; set; } public HashSet UserGroups { get; set; } - public IReadOnlyDictionary RadiusReplyAttributes { get; set; } + public IReadOnlyDictionary> RadiusReplyAttributes { get; set; } public IReadOnlyCollection Attributes { get; set; } - public (int min, int max)? InvalidCredentialDelay { get; set; } + public CredentialDelay? InvalidCredentialDelay { get; set; } public static SendAdapterResponseRequest FromContext(RadiusPipelineContext context) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs index 092460e1..07d8e4ce 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -83,7 +83,7 @@ private string GetFilter(UserIdentity identity, ILdapSchema schema) public ILdapSchema? LoadSchema(LdapConnectionData request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString), + var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString, true), AuthType.Basic, request.UserName, request.Password, @@ -152,7 +152,7 @@ private static ModifyRequest BuildPasswordChangeRequest(ILdapSchema ldapSchema, private ILdapConnection CreateConnection(LdapConnectionData data) { - var options = new LdapConnectionOptions(new LdapConnectionString(data.ConnectionString, true), + var options = new LdapConnectionOptions(new LdapConnectionString(data.ConnectionString, true, false), AuthType.Basic, data.UserName, data.Password, diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs index d8c49fa1..ec18ab8b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Exceptions/InvalidConfigurationException.cs @@ -8,7 +8,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Exceptions /// /// The Radius adapter configuration is invalid. /// -public class InvalidConfigurationException : Exception +internal class InvalidConfigurationException : Exception { public InvalidConfigurationException(string message) : base($"Configuration error: {message}") { } @@ -18,7 +18,7 @@ public InvalidConfigurationException(string message, string fileName) public InvalidConfigurationException(string message, Exception inner) : base($"Configuration error: {message}", inner) { } - public static InvalidConfigurationException For(Expression> propertySelector, + public static InvalidConfigurationException For(Expression> propertySelector, string formattedMessage, params object[] args) { @@ -40,7 +40,7 @@ public static InvalidConfigurationException For(Expression(Expression> propertySelector) + private static string Property(Expression> propertySelector) { if (propertySelector is null) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs index 25afa85c..0f7ae094 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -3,14 +3,13 @@ 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.Loader; 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.Shared.Extensions; -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations; +namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; public class ConfigurationLoader : IConfigurationLoader { @@ -85,7 +84,7 @@ private ClientConfiguration ParseClientConfiguration(string filePath) return clientConfig; } - private static ConfigurationFile ReadConfiguration(string filePath, string prefix = null) + private static AdapterConfiguration ReadConfiguration(string filePath, string prefix = null) { if (!File.Exists(filePath)) throw new InvalidConfigurationException($"Configuration file not found: {filePath}"); @@ -93,13 +92,13 @@ private static ConfigurationFile ReadConfiguration(string filePath, string prefi return ConfigurationReader.Read(filePath, prefix); } - private IReadOnlyDictionary ParseReplyAttributes( + private IReadOnlyDictionary> ParseReplyAttributes( RadiusReplySection radiusReplySection) { if (radiusReplySection?.Attributes?.Any() != true) - return new Dictionary(); + return new Dictionary>(); - var result = new Dictionary(); + var result = new Dictionary>(); var groupedAttributes = radiusReplySection.Attributes .Where(a => !string.IsNullOrWhiteSpace(a.Name)) @@ -109,7 +108,7 @@ private IReadOnlyDictionary ParseReplyAttribute { var attributes = group .Select(CreateReplyAttribute) - .ToArray(); + .ToList(); result[group.Key] = attributes; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs index 11ba3966..cca2ad71 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ClientConfiguration.cs @@ -7,7 +7,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; -public class ClientConfiguration : IClientConfiguration +internal class ClientConfiguration : IClientConfiguration { public string Name { get; set; } @@ -21,22 +21,22 @@ public class ClientConfiguration : IClientConfiguration public IPAddress? RadiusClientIp { get; set; } public string RadiusClientNasIdentifier { get; set; } public string RadiusSharedSecret { get; set; } - public IPEndPoint[] NpsServerEndpoints { get; set; } + public IReadOnlyList NpsServerEndpoints { get; set; } public TimeSpan NpsServerTimeout { get; set; } - public (PrivacyMode PrivacyMode, string[] PrivacyFields) Privacy { get; set; } + public Privacy Privacy { get; set; } public PreAuthMode? PreAuthenticationMethod { get; set; } public TimeSpan AuthenticationCacheLifetime { get; set; } = TimeSpan.Zero; - public (int min, int max)? InvalidCredentialDelay { get; set; } + 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 IReadOnlyDictionary>? ReplyAttributes { get; set; } - public static ClientConfiguration FromConfiguration(ConfigurationFile configurationFile) + public static ClientConfiguration FromConfiguration(AdapterConfiguration configurationFile) { ArgumentNullException.ThrowIfNull(configurationFile); var dto = new ClientConfiguration @@ -44,23 +44,23 @@ public static ClientConfiguration FromConfiguration(ConfigurationFile configurat 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.MultifactorNasIdentifier : + 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), - SignUpGroups = ConfigurationValueProcessor.TryParseStringList(configurationFile.AppSettings.SignUpGroups, out var list) ? list : [], + SignUpGroups = ConfigurationValueParser.TryParseStringList(configurationFile.AppSettings.SignUpGroups, out var list) ? list : [], BypassSecondFactorWhenApiUnreachable = configurationFile.AppSettings.BypassSecondFactorWhenApiUnreachable, - RadiusClientIp = ConfigurationValueProcessor.TryParseIpAddress(configurationFile.AppSettings.RadiusClientIp, out var address) ? address : null, + RadiusClientIp = ConfigurationValueParser.TryParseIpAddress(configurationFile.AppSettings.RadiusClientIp, out var address) ? address : null, RadiusClientNasIdentifier = configurationFile.AppSettings.RadiusClientNasIdentifier, 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), - NpsServerEndpoints = ConfigurationValueProcessor.TryParseEndpoints(configurationFile.AppSettings.NpsServerEndpoints, out var npsServerEndpoints) + NpsServerEndpoints = ConfigurationValueParser.TryParseEndpoints(configurationFile.AppSettings.NpsServerEndpoints, out var npsServerEndpoints) ? npsServerEndpoints : [], - NpsServerTimeout = ConfigurationValueProcessor.TryParseTimeout(configurationFile.AppSettings.NpsServerTimeout, out var timeout) ? timeout.Value : TimeSpan.Parse("00:00:05"), - Privacy = ConfigurationValueProcessor.TryParsePrivacyModeWithFields(configurationFile.AppSettings.Privacy, out var privacy) ? privacy : new(PrivacyMode.None, []), - PreAuthenticationMethod = ConfigurationValueProcessor.TryParseEnum(configurationFile.AppSettings.PreAuthenticationMethod, out var mode) ? mode : PreAuthMode.None, - AuthenticationCacheLifetime = ConfigurationValueProcessor.TryParseTimeSpan(configurationFile.AppSettings.AuthenticationCacheLifetime, out var span) ? span : TimeSpan.Zero, + NpsServerTimeout = ConfigurationValueParser.TryParseTimeout(configurationFile.AppSettings.NpsServerTimeout, out var timeout) ? timeout.Value : TimeSpan.Parse("00:00:05"), + Privacy = ConfigurationValueParser.TryParsePrivacyModeWithFields(configurationFile.AppSettings.Privacy, out var privacy) ? privacy : new(PrivacyMode.None, []), + PreAuthenticationMethod = ConfigurationValueParser.TryParseEnum(configurationFile.AppSettings.PreAuthenticationMethod, out var mode) ? mode : PreAuthMode.None, + AuthenticationCacheLifetime = ConfigurationValueParser.TryParseTimeSpan(configurationFile.AppSettings.AuthenticationCacheLifetime, out var span) ? span : TimeSpan.Zero, CallingStationIdAttribute = configurationFile.AppSettings.CallingStationIdAttribute, - IpWhiteList = ConfigurationValueProcessor.TryParseIpRanges(configurationFile.AppSettings.IpWhiteList, out var ipWhiteList) ? ipWhiteList : [], - InvalidCredentialDelay = ConfigurationValueProcessor.TryParseDelaySettings(configurationFile.AppSettings.InvalidCredentialDelay, out var tuple) + IpWhiteList = ConfigurationValueParser.TryParseIpRanges(configurationFile.AppSettings.IpWhiteList, out var ipWhiteList) ? ipWhiteList : [], + InvalidCredentialDelay = ConfigurationValueParser.TryParseDelaySettings(configurationFile.AppSettings.InvalidCredentialDelay, out var tuple) ? tuple : null }; @@ -71,16 +71,17 @@ public static ClientConfiguration FromConfiguration(ConfigurationFile configurat : throw InvalidConfigurationException.For(prop => prop.AppSettings.FirstFactorAuthenticationSource, "Property '{prop}' is required. Config name: '{0}'", configurationFile.FileName); - dto.FirstFactorAuthenticationSource = ConfigurationValueProcessor.TryParseEnum(firstFactorAuthenticationSource, out var source) + dto.FirstFactorAuthenticationSource = ConfigurationValueParser.TryParseEnum(firstFactorAuthenticationSource, out var source) ? source - : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{0}'", configurationFile.FileName); ; + : throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Value: {0}. Config name: '{1}'", firstFactorAuthenticationSource, configurationFile.FileName); ; - var adapterClientEndpoint = !string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint) ? configurationFile.AppSettings.AdapterClientEndpoint : + 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); - dto.AdapterClientEndpoint = - ConfigurationValueProcessor.TryParseEndpoint(adapterClientEndpoint, out var endpoint) + if(!string.IsNullOrWhiteSpace(configurationFile.AppSettings.AdapterClientEndpoint)) + dto.AdapterClientEndpoint = + ConfigurationValueParser.TryParseEndpoint(adapterClientEndpoint, out var endpoint) ? endpoint! : - throw InvalidConfigurationException.For(c => c.AppSettings.FirstFactorAuthenticationSource, "Error while cast property '{prop}'. Config name: '{0}'", configurationFile.FileName); ; + 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.Infrastructure/Configurations/Models/ConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs index 497bf5fc..2d4a7861 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs @@ -1,12 +1,11 @@ using System.ComponentModel; using System.Xml.Serialization; -using Microsoft.Extensions.Configuration; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; -public class ConfigurationFile +internal class AdapterConfiguration { - public ConfigurationFile() + public AdapterConfiguration() { AppSettings = new AppSettingsSection(); LdapServers = new List(); @@ -20,7 +19,7 @@ public ConfigurationFile() public RadiusReplySection RadiusReply { get; set; } = new(); } -public class AppSettingsSection +internal class AppSettingsSection { [Description("multifactor-api-url")] public string MultifactorApiUrl { get; set; } @@ -91,7 +90,7 @@ public class AppSettingsSection public string IpWhiteList { get; set; } } -public class LdapServerSection +internal class LdapServerSection { [Description("connection-string")] public required string ConnectionString { get; set; } @@ -135,14 +134,14 @@ public class LdapServerSection public string BypassSecondFactorWhenApiUnreachableGroups { get; set; } } -public class RadiusReplySection +internal class RadiusReplySection { [XmlArray("Attributes")] [XmlArrayItem("add")] public List Attributes { get; set; } } -public class RadiusAttributeItem +internal class RadiusAttributeItem { [Description("name")] public string Name { get; set; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs index d985bc7d..9552fdfc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs @@ -5,7 +5,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; -public class LdapServerConfiguration : ILdapServerConfiguration +internal class LdapServerConfiguration : ILdapServerConfiguration { public string ConnectionString { get; init; } public string Username { get; init; } @@ -43,30 +43,30 @@ public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapSe BindTimeoutSeconds = ldapServerSection.BindTimeoutSeconds ?? 30, AccessGroups = - ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.AccessGroups, + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.AccessGroups, out var accessGroups) ? accessGroups : [], SecondFaGroups = - ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.SecondFaGroups, + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.SecondFaGroups, out var secondFaGroups) ? secondFaGroups : [], SecondFaBypassGroups = - ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.SecondFaBypassGroups, out var secondFaBypassGroups) + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.SecondFaBypassGroups, out var secondFaBypassGroups) ? secondFaBypassGroups : [], LoadNestedGroups =ldapServerSection.LoadNestedGroups, NestedGroupsBaseDns = - ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDns, out var nestedGroupsBaseDns) + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDns, out var nestedGroupsBaseDns) ? nestedGroupsBaseDns : [], AuthenticationCacheGroups = - ConfigurationValueProcessor.TryParseDistinguishedNames(ldapServerSection.AuthenticationCacheGroups, out var authenticationCacheGroups) + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.AuthenticationCacheGroups, out var authenticationCacheGroups) ? authenticationCacheGroups : [], PhoneAttributes = - ConfigurationValueProcessor.TryParseStringList(ldapServerSection.PhoneAttributes, + ConfigurationValueParser.TryParseStringList(ldapServerSection.PhoneAttributes, out var phoneAttributes) ? phoneAttributes : [], @@ -75,26 +75,26 @@ public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapSe TrustedDomainsEnabled = ldapServerSection.TrustedDomainsEnabled, AlternativeSuffixesEnabled =ldapServerSection.AlternativeSuffixesEnabled, IncludedDomains = - ConfigurationValueProcessor.TryParseStringList(ldapServerSection.IncludedDomains, + ConfigurationValueParser.TryParseStringList(ldapServerSection.IncludedDomains, out var includedDomains) ? includedDomains : [], ExcludedDomains = - ConfigurationValueProcessor.TryParseStringList(ldapServerSection.ExcludedDomains, + ConfigurationValueParser.TryParseStringList(ldapServerSection.ExcludedDomains, out var excludedDomains) ? excludedDomains : [], IncludedSuffixes = - ConfigurationValueProcessor.TryParseStringList(ldapServerSection.IncludedSuffixes, + ConfigurationValueParser.TryParseStringList(ldapServerSection.IncludedSuffixes, out var includedSuffixes) ? includedSuffixes : [], ExcludedSuffixes = - ConfigurationValueProcessor.TryParseStringList(ldapServerSection.ExcludedSuffixes, + ConfigurationValueParser.TryParseStringList(ldapServerSection.ExcludedSuffixes, out var excludedSuffixes) ? excludedSuffixes : [], - BypassSecondFactorWhenApiUnreachableGroups = ConfigurationValueProcessor.TryParseStringList(ldapServerSection.BypassSecondFactorWhenApiUnreachableGroups, + BypassSecondFactorWhenApiUnreachableGroups = ConfigurationValueParser.TryParseStringList(ldapServerSection.BypassSecondFactorWhenApiUnreachableGroups, out var bypassSecondFactorWhenApiUnreachableGroups) ? bypassSecondFactorWhenApiUnreachableGroups : [] diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs index d0f6294a..741e54ab 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/RootConfiguration.cs @@ -5,7 +5,7 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models; -public class RootConfiguration : IRootConfiguration +internal class RootConfiguration : IRootConfiguration { public IReadOnlyList MultifactorApiUrls { get; set; } public string? MultifactorApiProxy { get; set; } @@ -25,13 +25,13 @@ public class RootConfiguration : IRootConfiguration public string? FileLogOutputTemplate { get; set; } public int LogFileMaxSizeBytes { get; set; } - public static RootConfiguration FromConfiguration(ConfigurationFile configurationFile) + public static RootConfiguration FromConfiguration(AdapterConfiguration configurationFile) { ArgumentNullException.ThrowIfNull(configurationFile); var conf = new RootConfiguration { MultifactorApiProxy = configurationFile.AppSettings?.MultifactorApiProxy, - MultifactorApiTimeout = ConfigurationValueProcessor.TryParseTimeout( + MultifactorApiTimeout = ConfigurationValueParser.TryParseTimeout( configurationFile.AppSettings?.MultifactorApiTimeout, out var span) ? span!.Value : TimeSpan.FromSeconds(65), LoggingFormat = configurationFile.AppSettings?.LoggingFormat, @@ -49,12 +49,12 @@ public static RootConfiguration FromConfiguration(ConfigurationFile configuratio }; 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 = ConfigurationValueProcessor.TryParseUrls(urls, out var parsedUrls) ? parsedUrls : + 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 = ConfigurationValueProcessor.TryParseEndpoint(endpoint, out var point) + conf.AdapterServerEndpoint = ConfigurationValueParser.TryParseEndpoint(endpoint, out var point) ? point : throw new InvalidConfigurationException($"Invalid 'adapter-server-endpoint': '{endpoint}'", configurationFile.FileName); return conf; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs similarity index 94% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs index 685adffc..df5dac04 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Parser/ConfigurationValueParser.cs @@ -1,13 +1,14 @@ 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.Configuration.Models.Enum; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using NetTools; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; -public static class ConfigurationValueProcessor +internal static class ConfigurationValueParser { public static bool TryParseEnum(string? value, out T result, T defaultValue = default) where T : struct { @@ -241,9 +242,9 @@ public static bool TryParseDistinguishedNames(string? value, out IReadOnlyList x < 0)) return false; - result = (values[0], values[1]); + 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 index 9845ed1b..ab8508fe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationBuilderExtensions.cs @@ -4,14 +4,14 @@ namespace Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Reader; public static class ConfigurationBuilderExtensions { - public static IConfigurationBuilder AddLegacyXmlConfig( + public static IConfigurationBuilder AddXmlConfig( this IConfigurationBuilder builder, string path) { return builder.Add(new XmlConfigurationSource(path)); } - public static IConfigurationBuilder AddPrefixEnvironmentVariables( + public static IConfigurationBuilder AddEnvironmentVariables( this IConfigurationBuilder builder, string prefix) { diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs index 7f861ad8..88bf768c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Reader/ConfigurationReader.cs @@ -1,27 +1,28 @@ 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; -public static class ConfigurationReader +internal static class ConfigurationReader { - public static ConfigurationFile Read(string filePath, string prefix = null) + internal static AdapterConfiguration? Read(string filePath, string prefix = null) { var builder = new ConfigurationBuilder() - .AddLegacyXmlConfig(filePath) - .AddPrefixEnvironmentVariables($"RAD_{prefix}") + .AddXmlConfig(filePath) + .AddEnvironmentVariables($"RAD_{prefix}") .Build(); try { - var config = builder.Get(); + var config = builder.Get(); + if (config == null) return null; config.FileName = Path.GetFileNameWithoutExtension(filePath); return config; } catch (Exception ex) { - Console.WriteLine($"\n=== Ошибка при Get(): {ex.Message} ==="); - Console.WriteLine("Подробности: " + ex.InnerException?.Message); + StartupLogger.Error(ex, "Error reading configuration file:{0}", ex.Message); throw; } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs index 471293a7..324fa9ad 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -165,7 +165,6 @@ public static void AddInfraServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddSingleton(); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index b399040f..b67c1473 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -40,12 +40,11 @@ public static LoggerConfiguration CreateLogger(LoggerConfiguration loggerConfigu rootConfiguration.SyslogUseTls ); var level = rootConfiguration.LoggingLevel; - var skip = string.IsNullOrWhiteSpace(level); - // if (string.IsNullOrWhiteSpace(level)) - // { - // throw new InvalidConfigurationException( - // string.Concat("'{prop}' element not found. Config name: '{0}'", "rootConfiguration.ConfigurationName")); - // } + if (string.IsNullOrWhiteSpace(level)) + { + throw new InvalidConfigurationException( + string.Concat("'{prop}' element not found. Config name: '{0}'", "rootConfiguration.ConfigurationName")); + } SetLogLevel(levelSwitch, level); @@ -74,10 +73,8 @@ private static void ConfigureLogging( 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)); + Log.Logger.Warning( + "The 'logging-format' parameter cannot be used together with the template. The 'file-log-output-template' parameter will be ignored."); } return; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs index aef234b4..06234c1b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/StartupLogger.cs @@ -18,20 +18,20 @@ public static class StartupLogger { SelfLog.Enable(Console.WriteLine); - // var baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); - // var dir = Path.Combine(baseDir!, LogDirectory); - // if (!Directory.Exists(dir)) - // { - // Directory.CreateDirectory(dir); - // } - - // var path = Path.Combine(dir, StartupLogFile); + var baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); + var dir = Path.Combine(baseDir!, LogDirectory); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var path = Path.Combine(dir, StartupLogFile); var loggerConfig = new LoggerConfiguration() - // .WriteTo.File(path: path, - // LogEventLevel.Verbose, - // FileLogTemplate, - // fileSizeLimitBytes: FileSizeLimitBytes, - // rollOnFileSizeLimit: true) + .WriteTo.File(path: path, + LogEventLevel.Verbose, + FileLogTemplate, + fileSizeLimitBytes: FileSizeLimitBytes, + rollOnFileSizeLimit: true) .WriteTo.Console(LogEventLevel.Verbose, ConsoleLogTemplate) .Enrich.FromLogContext(); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs deleted file mode 100644 index a20d68b5..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/IRadiusAttributeSerializer.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; - -public interface IRadiusAttributeSerializer -{ - byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null); -} diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs deleted file mode 100644 index c66b706a..00000000 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusAttributeSerializer.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Net; -using System.Text; -using Microsoft.Extensions.Logging; -using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; -using Multifactor.Radius.Adapter.v2.Application.Features.Security; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary.Attributes; - -namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; - - -public class RadiusAttributeSerializer : IRadiusAttributeSerializer -{ - private readonly IRadiusDictionary _radiusDictionary; - private readonly ILogger _logger; - - public RadiusAttributeSerializer( - IRadiusDictionary radiusDictionary, - ILogger logger) - { - _radiusDictionary = radiusDictionary; - _logger = logger; - } - - public byte[]? Serialize(string attributeName, object value, RadiusAuthenticator authenticator, SharedSecret sharedSecret, RadiusAuthenticator? requestAuthenticator = null) - { - try - { - var attributeDefinition = _radiusDictionary.GetAttribute(attributeName); - if (attributeDefinition == null) - { - _logger.LogWarning("Unknown attribute: {AttributeName}", attributeName); - return null; - } - - byte[] contentBytes = ConvertValueToBytes(value, attributeDefinition.Type); - - // Special handling for certain attributes - if (attributeDefinition.Code == 2) // User-Password - { - contentBytes = RadiusPasswordProtector.Encrypt(sharedSecret, authenticator, contentBytes); - } - else if (attributeDefinition.Code == 80) // Message-Authenticator - { - // Will be calculated later, fill with zeros for now - contentBytes = new byte[16]; - } - - byte[] headerBytes; - if (attributeDefinition is DictionaryVendorAttribute vendorAttribute) - { - headerBytes = CreateVendorSpecificHeader(vendorAttribute, contentBytes.Length); - } - else - { - headerBytes = CreateStandardHeader(attributeDefinition.Code, contentBytes.Length); - } - - var result = new byte[headerBytes.Length + contentBytes.Length]; - Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); - Buffer.BlockCopy(contentBytes, 0, result, headerBytes.Length, contentBytes.Length); - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to serialize attribute: {AttributeName}", attributeName); - return null; - } - } - - private byte[] ConvertValueToBytes(object value, string type) - { - switch (type.ToLowerInvariant()) - { - case "string": - case "tagged-string": - return Encoding.UTF8.GetBytes(value.ToString() ?? string.Empty); - - case "octets": - if (value is byte[] bytes) - return bytes; - if (value is string str) - return Encoding.UTF8.GetBytes(str); - throw new ArgumentException($"Cannot convert {value.GetType()} to octets"); - - case "integer": - case "tagged-integer": - return ConvertIntegerToBytes(value); - - case "ipaddr": - if (value is IPAddress ip) - return ip.GetAddressBytes(); - if (value is string ipStr) - return IPAddress.Parse(ipStr).GetAddressBytes(); - throw new ArgumentException($"Cannot convert {value.GetType()} to IP address"); - - case "date": - return ConvertDateToBytes(value); - - default: - _logger.LogWarning("Unknown attribute type: {Type}", type); - if (value is byte[] byteArray) - return byteArray; - return Encoding.UTF8.GetBytes(value.ToString() ?? string.Empty); - } - } - - private static byte[] ConvertIntegerToBytes(object value) - { - int intValue; - - switch (value) - { - case int i: - intValue = i; - break; - case uint ui: - intValue = (int)ui; - break; - case short s: - intValue = s; - break; - case ushort us: - intValue = us; - break; - case byte b: - intValue = b; - break; - case string str when int.TryParse(str, out int parsed): - intValue = parsed; - break; - default: - throw new ArgumentException($"Cannot convert {value.GetType()} to integer"); - } - - var bytes = BitConverter.GetBytes(intValue); - Array.Reverse(bytes); - return bytes; - } - - private static byte[] ConvertDateToBytes(object value) - { - DateTime date; - - if (value is DateTime dt) - { - date = dt; - } - else if (value is string str && DateTime.TryParse(str, out var parsed)) - { - date = parsed; - } - else - { - date = DateTime.UtcNow; - } - - var unixTime = (uint)(date - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; - var bytes = BitConverter.GetBytes(unixTime); - Array.Reverse(bytes); - return bytes; - } - - private static byte[] CreateStandardHeader(byte typeCode, int contentLength) - { - var header = new byte[2]; - header[0] = typeCode; - header[1] = (byte)(2 + contentLength); // Total length: header (2) + content - return header; - } - - private static byte[] CreateVendorSpecificHeader(DictionaryVendorAttribute vendorAttribute, int contentLength) - { - // VSA format: Type(1)=26, Length(1), Vendor-Id(4), Vendor-Type(1), Vendor-Length(1), Content - var header = new byte[8]; - - header[0] = 26; // Vendor-Specific attribute type - header[1] = (byte)(8 + contentLength); // Total VSA length - - var vendorIdBytes = BitConverter.GetBytes(vendorAttribute.VendorId); - Array.Reverse(vendorIdBytes); - Buffer.BlockCopy(vendorIdBytes, 0, header, 2, 4); - - header[6] = (byte)vendorAttribute.VendorCode; - header[7] = (byte)(2 + contentLength); // Vendor-specific part length - - return header; - } -} \ 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 index e1b711d3..496c6a96 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -13,8 +13,6 @@ public class RadiusPacketBuilder : IRadiusPacketBuilder { private readonly IRadiusDictionary _radiusDictionary; private readonly IRadiusCryptoProvider _cryptoProvider; - // private readonly ILogger _logger; - // private readonly IRadiusAttributeSerializer _attributeSerializer; /// @@ -34,14 +32,10 @@ public class RadiusPacketBuilder : IRadiusPacketBuilder public RadiusPacketBuilder( IRadiusDictionary radiusDictionary, - IRadiusCryptoProvider cryptoProvider, - IRadiusAttributeSerializer attributeSerializer, - ILogger logger) + IRadiusCryptoProvider cryptoProvider) { _radiusDictionary = radiusDictionary ?? throw new ArgumentNullException(nameof(radiusDictionary)); _cryptoProvider = cryptoProvider ?? throw new ArgumentNullException(nameof(cryptoProvider)); - // _attributeSerializer = attributeSerializer ?? throw new ArgumentNullException(nameof(attributeSerializer)); - // _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public byte[] Build(RadiusPacket packet, SharedSecret sharedSecret) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs index 34f5ef27..6c9d5122 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Parsers/RadiusAttributeParser.cs @@ -58,7 +58,7 @@ public RadiusAttributeParser( byte vendorType = contentBytes[4]; byte vendorLength = contentBytes[5]; - if (vendorLength < 2 || 2 + vendorLength - 2 > contentBytes.Length) + if (vendorLength < 2 || vendorLength > contentBytes.Length) return null; byte[] vendorContentBytes = new byte[vendorLength - 2]; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs index 41ba8621..316585da 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Sender/AdapterResponseSender.cs @@ -228,11 +228,11 @@ private async Task SendResponsePacketAsync(RadiusPacket responsePacket, SendAdap // Задержка для AccessReject (security feature) if (responsePacket.Code == PacketCode.AccessReject - && request.InvalidCredentialDelay.HasValue) + && request.InvalidCredentialDelay != null) { await WaitSomeTimeAsync( - request.InvalidCredentialDelay.Value.min, - request.InvalidCredentialDelay.Value.max); + request.InvalidCredentialDelay.Min, + request.InvalidCredentialDelay.Max); } await _udpClient.SendAsync(bytes, bytes.Length, endpoint); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs index fcff7bbf..0dc9abb3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -49,7 +49,7 @@ public IDictionary> GetReplyAttributes(GetReplyAttributesRe private List ProcessAttribute( string attributeName, - IRadiusReplyAttribute[] attributeValues, + IReadOnlyList attributeValues, GetReplyAttributesRequest request) { var result = new List(); @@ -157,7 +157,7 @@ private static bool MatchesUserGroupCondition(IReadOnlyList conditions, return [attributeValue.Value]; } - private static bool IsSufficientAttribute(IRadiusReplyAttribute[] attributeValues) + private static bool IsSufficientAttribute(IReadOnlyList attributeValues) { return attributeValues.Any(av => av.Sufficient); } 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 index 2c504649..9df516cf 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/RadiusFirstFactorProcessorTests.cs @@ -1,7 +1,6 @@ using System.Net; 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.FirstFactor; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; @@ -16,7 +15,6 @@ namespace Multifactor.Radius.Adapter.v2.Tests.Application.Pipeline.FirstFactor public class RadiusFirstFactorProcessorTests { private readonly Mock _radiusPacketServiceMock; - private readonly Mock _radiusClientFactoryMock; private readonly Mock> _loggerMock; private readonly Mock _radiusClientMock; private readonly RadiusFirstFactorProcessor _processor; @@ -24,16 +22,16 @@ public class RadiusFirstFactorProcessorTests public RadiusFirstFactorProcessorTests() { _radiusPacketServiceMock = new Mock(); - _radiusClientFactoryMock = new Mock(); + var radiusClientFactoryMock = new Mock(); _loggerMock = new Mock>(); _radiusClientMock = new Mock(); _processor = new RadiusFirstFactorProcessor( _radiusPacketServiceMock.Object, - _radiusClientFactoryMock.Object, + radiusClientFactoryMock.Object, _loggerMock.Object); - _radiusClientFactoryMock + radiusClientFactoryMock .Setup(x => x.CreateRadiusClient(It.IsAny())) .Returns(_radiusClientMock.Object); } @@ -49,8 +47,9 @@ public void AuthenticationSource_ShouldBeRadius() public async Task ProcessFirstFactor_ShouldRejectWhenUserNameMissing() { // Arrange + var clientConfiguration = CreateTestClientConfiguration(); var requestPacket = CreateRadiusPacket(userName: null); - var context = CreateContext(requestPacket); + var context = CreateContext(requestPacket, clientConfiguration); // Act await _processor.ProcessFirstFactor(context); @@ -72,7 +71,8 @@ public async Task ProcessFirstFactor_ShouldAcceptWhenRadiusAccepts() { // Arrange var requestPacket = CreateRadiusPacket("testuser"); - var context = CreateContext(requestPacket); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateContext(requestPacket, clientConfiguration); var responsePacket = new RadiusPacket( new RadiusPacketHeader(PacketCode.AccessAccept, 1, new byte[16])); @@ -100,7 +100,8 @@ public async Task ProcessFirstFactor_ShouldRejectWhenRadiusRejects() { // Arrange var requestPacket = CreateRadiusPacket("testuser"); - var context = CreateContext(requestPacket); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateContext(requestPacket, clientConfiguration); var responsePacket = new RadiusPacket( new RadiusPacketHeader(PacketCode.AccessReject, 1, new byte[16])); @@ -118,14 +119,16 @@ public async Task ProcessFirstFactor_ShouldRejectWhenRadiusRejects() public async Task ProcessFirstFactor_ShouldTryNextServerWhenFirstFails() { // Arrange - var requestPacket = CreateRadiusPacket("testuser"); - var context = CreateContext(requestPacket); - - context.ClientConfiguration.NpsServerEndpoints = new[] + 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])); @@ -167,14 +170,14 @@ public async Task ProcessFirstFactor_ShouldTryNextServerWhenFirstFails() public async Task ProcessFirstFactor_ShouldRejectWhenAllServersFail() { // Arrange - var requestPacket = CreateRadiusPacket("testuser"); - var context = CreateContext(requestPacket); - - context.ClientConfiguration.NpsServerEndpoints = new[] + 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( @@ -235,16 +238,21 @@ private RadiusPacket CreateRadiusPacket(string userName) return packet; } - private RadiusPipelineContext CreateContext(RadiusPacket requestPacket) + private static ClientConfiguration CreateTestClientConfiguration(IPEndPoint[]? npsServerEndpoints = null) { - var clientConfig = new ClientConfiguration + return new ClientConfiguration { Name = "TestClient", RadiusSharedSecret = "shared-secret", AdapterClientEndpoint = new IPEndPoint(IPAddress.Any, 0), - NpsServerEndpoints = new[] { new IPEndPoint(IPAddress.Parse("192.168.1.1"), 1812) }, + 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) { 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 index 9d796183..b141963a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/FirstFactorStepTests.cs @@ -45,7 +45,8 @@ await Assert.ThrowsAsync(() => public async Task ExecuteAsync_WhenStatusNotAwaiting_DoesNothing() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Accept; // Not Awaiting // Act @@ -61,7 +62,8 @@ public async Task ExecuteAsync_WhenStatusNotAwaiting_DoesNothing() public async Task ExecuteAsync_WhenStatusAwaiting_GetsAndRunsProcessor() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; var processorMock = new Mock(); @@ -86,7 +88,8 @@ public async Task ExecuteAsync_WhenStatusAwaiting_GetsAndRunsProcessor() public async Task ExecuteAsync_WhenMustChangePasswordDomainEmpty_NoChallenge() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; context.MustChangePasswordDomain = ""; // Empty @@ -108,7 +111,8 @@ public async Task ExecuteAsync_WhenMustChangePasswordDomainEmpty_NoChallenge() public async Task ExecuteAsync_WhenMustChangePasswordDomainSet_CreatesChallenge() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; context.MustChangePasswordDomain = "test-domain"; @@ -141,7 +145,8 @@ public async Task ExecuteAsync_WhenMustChangePasswordDomainSet_CreatesChallenge( public async Task ExecuteAsync_WhenChallengeProcessorNotFound_ThrowsException() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; context.MustChangePasswordDomain = "test-domain"; @@ -165,7 +170,8 @@ public async Task ExecuteAsync_WhenChallengeProcessorNotFound_ThrowsException() public async Task ExecuteAsync_WhenFirstFactorReject_Terminates() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; var processorMock = new Mock(); @@ -187,7 +193,8 @@ public async Task ExecuteAsync_WhenFirstFactorReject_Terminates() public async Task ExecuteAsync_WhenFirstFactorAccept_DoesNotTerminate() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; var processorMock = new Mock(); @@ -209,7 +216,8 @@ public async Task ExecuteAsync_WhenFirstFactorAccept_DoesNotTerminate() public async Task ExecuteAsync_LogsDebugMessage() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; var processorMock = new Mock(); @@ -244,9 +252,9 @@ public async Task ExecuteAsync_LogsDebugMessage() public async Task ExecuteAsync_WithLdapSource_GetsCorrectProcessor() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(AuthenticationSource.Ldap); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; - context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.Ldap; var processorMock = new Mock(); _processorProviderMock @@ -266,9 +274,9 @@ public async Task ExecuteAsync_WithLdapSource_GetsCorrectProcessor() public async Task ExecuteAsync_WithNoneSource_GetsCorrectProcessor() { // Arrange - var context = CreateTestContext(); + var clientConfiguration = CreateTestClientConfiguration(AuthenticationSource.None); + var context = CreateTestContext(clientConfiguration); context.FirstFactorStatus = AuthenticationStatus.Awaiting; - context.ClientConfiguration.FirstFactorAuthenticationSource = AuthenticationSource.None; var processorMock = new Mock(); _processorProviderMock @@ -284,19 +292,22 @@ public async Task ExecuteAsync_WithNoneSource_GetsCorrectProcessor() Times.Once); } - private static RadiusPipelineContext CreateTestContext() + private static IClientConfiguration CreateTestClientConfiguration(AuthenticationSource source = AuthenticationSource.Radius) { - var clientConfig = new ClientConfiguration + 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, clientConfig); + return new RadiusPipelineContext(requestPacket, clientConfiguration); } } } \ 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 index 76ba2e36..88d73e73 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/ProfileLoadingStepTests.cs @@ -156,7 +156,7 @@ public async Task ExecuteAsync_IncludesCorrectAttributes() // Add some reply attributes var replyAttribute = new RadiusReplyAttribute { Name = "department" }; - context.ClientConfiguration.ReplyAttributes = new Dictionary + context.ClientConfiguration.ReplyAttributes = new Dictionary { ["TestAttribute"] = [replyAttribute] }; @@ -230,7 +230,7 @@ private static RadiusPipelineContext CreateTestContext(LdapServerConfiguration? { Name = "test-client", RadiusSharedSecret = "test-secret", - ReplyAttributes = new Dictionary() + ReplyAttributes = new Dictionary>() }; var requestPacket = new RadiusPacket( new RadiusPacketHeader(PacketCode.AccessRequest, 1, new byte[16])) 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 index 4806b828..afb89db9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs @@ -317,10 +317,12 @@ private static RadiusPipelineContext CreateTestContext() var ldapProfileMock = new Mock(); ldapProfileMock.Setup(x => x.MemberOf).Returns(new List()); - var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig); - context.LdapProfile = ldapProfileMock.Object; - context.SecondFactorStatus = AuthenticationStatus.Awaiting; - + var context = new RadiusPipelineContext(requestPacket, clientConfig, ldapConfig) + { + LdapProfile = ldapProfileMock.Object, + SecondFactorStatus = AuthenticationStatus.Awaiting + }; + return context; } } diff --git a/src/Multifactor.Radius.Adapter.v2/Program.cs b/src/Multifactor.Radius.Adapter.v2/Program.cs index fffa2666..c49aa90a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Program.cs +++ b/src/Multifactor.Radius.Adapter.v2/Program.cs @@ -42,13 +42,13 @@ } catch (Exception ex) { - if(ex is InvalidConfigurationException) - StartupLogger.Error(null, "Unable to start: {Message:l}", ex.Message); - else - { + // 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 d7554f8b..3ad1175a 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/AdapterServer.cs @@ -6,7 +6,7 @@ namespace Multifactor.Radius.Adapter.v2.Server; -public class AdapterServer : IAsyncDisposable +internal sealed class AdapterServer : IAsyncDisposable { private readonly IUdpClient _udpClient; private readonly IRadiusUdpAdapter _packetAdapter; @@ -28,10 +28,10 @@ public AdapterServer( ServiceConfiguration serviceConfiguration, ILogger logger) { - _udpClient = udpClient ?? throw new ArgumentNullException(nameof(udpClient)); - _packetAdapter = packetAdapter ?? throw new ArgumentNullException(nameof(packetAdapter)); - _serviceConfiguration = serviceConfiguration ?? throw new ArgumentNullException(nameof(serviceConfiguration)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _udpClient = udpClient; + _packetAdapter = packetAdapter; + _serviceConfiguration = serviceConfiguration; + _logger = logger; _concurrencyLimiter = new SemaphoreSlim(MaxConcurrentRequests); } @@ -127,7 +127,8 @@ private async Task StopAsync(CancellationToken cancellationToken = default) { _logger.LogInformation("Stopping RADIUS server..."); - await _cts?.CancelAsync(); + if(_cts != null) + await _cts.CancelAsync(); if (_receiveLoopTask != null) { diff --git a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs index 4de16010..f3651079 100644 --- a/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs +++ b/src/Multifactor.Radius.Adapter.v2/Server/ServerHost.cs @@ -3,7 +3,7 @@ namespace Multifactor.Radius.Adapter.v2.Server; -public class ServerHost : IHostedService +internal sealed class ServerHost : IHostedService { private readonly AdapterServer _server; private readonly ILogger _logger; @@ -40,7 +40,8 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _logger.LogInformation("Stopping RADIUS server host..."); try { - await _cts?.CancelAsync(); + if(_cts != null) + await _cts.CancelAsync(); if (_serverTask is { IsCompleted: false }) { From 25df3305469fe5f21ccfe51b2a27b38041247639 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Tue, 10 Feb 2026 08:08:05 +0300 Subject: [PATCH 11/11] BugFix --- .../Features/Ldap/Models/MembershipRequest.cs | 2 +- .../Multifactor/MultifactorApiService.cs | 2 +- .../Multifactor/RequestDataExtractor.cs | 2 +- .../Adapters/Ldap/LdapAdapter.cs | 6 +++-- .../Adapters/Udp/CustomUdpClient.cs | 23 +++---------------- .../Loader/ConfigurationLoader.cs | 6 ++++- ...urationFile.cs => AdapterConfiguration.cs} | 2 +- .../Models/LdapServerConfiguration.cs | 6 ++--- .../Extensions/InfrastructureExtensions.cs | 13 ++++------- .../Logging/SerilogLoggerFactory.cs | 1 - .../Radius/Builders/RadiusPacketBuilder.cs | 1 - .../Services/RadiusReplyAttributeService.cs | 1 - .../LdapFirstFactorProcessorTests.cs | 1 - .../NoneFirstFactorProcessorTests.cs | 1 - .../Pipeline/RadiusPipelineProviderTests.cs | 1 - .../Pipeline/RadiusPipelineTests.cs | 1 - .../Steps/AccessChallengeStepTests.cs | 1 - .../Steps/AccessRequestFilteringStepTests.cs | 1 - .../Pipeline/Steps/IpWhiteListStepTests.cs | 1 - .../Steps/LdapSchemaLoadingStepTests.cs | 1 - .../Pipeline/Steps/PreAuthPostCheckTests.cs | 1 - .../Pipeline/Steps/SecondFactorStepTests.cs | 1 - .../Radius/RadiusPacketProcessorTests.cs | 1 - src/Multifactor.Radius.Adapter.v2/Program.cs | 1 - 24 files changed, 24 insertions(+), 53 deletions(-) rename src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/{ConfigurationFile.cs => AdapterConfiguration.cs} (99%) 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 index d23343ab..f1176bfc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Ldap/Models/MembershipRequest.cs @@ -14,7 +14,7 @@ public class MembershipRequest public static MembershipRequest FromContext(RadiusPipelineContext context, IReadOnlyList groups) { - if (context.LdapConfiguration?.AccessGroups.Count == 0) + if (groups.Count == 0) throw new ArgumentNullException(); return new MembershipRequest diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs index 4d373c4e..60495bc8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/MultifactorApiService.cs @@ -34,7 +34,7 @@ public MultifactorApiService( 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)) { diff --git a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs index 72274359..c67f1c4b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs +++ b/src/Multifactor.Radius.Adapter.v2.Application/Features/Multifactor/RequestDataExtractor.cs @@ -29,7 +29,7 @@ public static PersonalData ExtractPersonalData(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));; + .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); } public static string? GetUserPhone(RadiusPipelineContext context) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs index 07d8e4ce..70a720e0 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Ldap/LdapAdapter.cs @@ -83,11 +83,13 @@ private string GetFilter(UserIdentity identity, ILdapSchema schema) public ILdapSchema? LoadSchema(LdapConnectionData request) { - var options = new LdapConnectionOptions(new LdapConnectionString(request.ConnectionString, true), + var options = new LdapConnectionOptions( + new LdapConnectionString(request.ConnectionString, true), AuthType.Basic, request.UserName, request.Password, - TimeSpan.FromSeconds(request.BindTimeoutInSeconds)); + TimeSpan.FromSeconds(request.BindTimeoutInSeconds) + ); return _schemaLoader.Load(options); } diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs index ea4a3e6b..5c1b4477 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Adapters/Udp/CustomUdpClient.cs @@ -23,26 +23,9 @@ public CustomUdpClient( _logger.LogInformation("UDP client initialized on {Endpoint}", endPoint); } - public async Task ReceiveAsync(CancellationToken cancellationToken = default) - { - try - { - return await _udpClient.ReceiveAsync(cancellationToken); - } - catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) - { - throw new OperationCanceledException("UDP receive was interrupted", ex, cancellationToken); - } - catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "UDP receive error"); - throw; - } - } + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + => await _udpClient.ReceiveAsync(cancellationToken); + public async Task SendAsync( byte[] datagram, diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs index 0f7ae094..87ba27c9 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Loader/ConfigurationLoader.cs @@ -7,6 +7,7 @@ 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; @@ -44,7 +45,7 @@ private RootConfiguration LoadRootConfiguration(string configPath) { if (!File.Exists(configPath)) throw new InvalidConfigurationException($"Root configuration not found: {configPath}"); - + StartupLogger.Debug($"Loading root configuration from {configPath}"); var config = ReadConfiguration(configPath); return RootConfiguration.FromConfiguration(config); } @@ -55,6 +56,7 @@ private List LoadClientConfigurations(string rootConfigPath if (!Directory.Exists(clientsPath)) { + StartupLogger.Debug($"Loading client configuration from {rootConfigPath}"); var clientConfig = ParseClientConfiguration(rootConfigPath); return [clientConfig]; } @@ -63,6 +65,7 @@ private List LoadClientConfigurations(string rootConfigPath if (clientConfigFiles.Length == 0) { + StartupLogger.Debug($"Loading client configuration from {rootConfigPath}"); var clientConfig = ParseClientConfiguration(rootConfigPath); return [clientConfig]; } @@ -74,6 +77,7 @@ private List LoadClientConfigurations(string rootConfigPath private ClientConfiguration ParseClientConfiguration(string filePath) { + StartupLogger.Debug($"Loading client configuration from {filePath}"); var prefix = GetConfigPrefix(filePath); var config = ReadConfiguration(filePath, prefix); diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs similarity index 99% rename from src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs rename to src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs index 2d4a7861..b245bf20 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/ConfigurationFile.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/AdapterConfiguration.cs @@ -109,7 +109,7 @@ internal class LdapServerSection [Description("load-nested-groups")] public bool LoadNestedGroups { get; set; } [Description("nested-groups-base-dn")] - public string NestedGroupsBaseDns { get; set; } + public string NestedGroupsBaseDn { get; set; } [Description("authentication-cache-groups")] public string AuthenticationCacheGroups { get; set; } [Description("phone-attributes")] diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs index 9552fdfc..06bfdd99 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Configurations/Models/LdapServerConfiguration.cs @@ -56,10 +56,10 @@ public static LdapServerConfiguration FromConfiguration(LdapServerSection ldapSe ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.SecondFaBypassGroups, out var secondFaBypassGroups) ? secondFaBypassGroups : [], - LoadNestedGroups =ldapServerSection.LoadNestedGroups, + LoadNestedGroups = ldapServerSection.LoadNestedGroups, NestedGroupsBaseDns = - ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDns, out var nestedGroupsBaseDns) - ? nestedGroupsBaseDns + ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.NestedGroupsBaseDn, out var nestedGroupsBaseDn) + ? nestedGroupsBaseDn : [], AuthenticationCacheGroups = ConfigurationValueParser.TryParseDistinguishedNames(ldapServerSection.AuthenticationCacheGroups, out var authenticationCacheGroups) diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs index 324fa9ad..a4fee8db 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -18,10 +18,8 @@ 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; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Loader; using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Models.Dictionary; -using Multifactor.Radius.Adapter.v2.Infrastructure.Configurations.Parser; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Builders; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Client; @@ -90,8 +88,7 @@ public static void AddMultifactorApi(this IServiceCollection services) client.Timeout = config.RootConfiguration.MultifactorApiTimeout; } }) - .AddPolicyHandler((serviceProvider, request) => - Policy + .AddPolicyHandler((serviceProvider, request) => Policy .Handle() .OrResult(response => !response.IsSuccessStatusCode && (int)response.StatusCode >= 500) .FallbackAsync( @@ -110,12 +107,12 @@ public static void AddMultifactorApi(this IServiceCollection services) }, onFallbackAsync: (outcome, context) => { - // var logger = serviceProvider.GetRequiredService(); - // logger.LogWarning("Primary endpoint failed. Trying fallback. Error: {Error}", - // outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); + var logger = serviceProvider.GetRequiredService(); + logger.LogWarning("Primary endpoint failed. Trying fallback. Error: {Error}", + outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()); return Task.CompletedTask; }) - .WrapAsync(Policy.TimeoutAsync(TimeSpan.FromSeconds(10))) + .WrapAsync(Policy.TimeoutAsync(serviceProvider.GetRequiredService().RootConfiguration.MultifactorApiTimeout)) ) .AddHttpMessageHandler() .ConfigurePrimaryHttpMessageHandler(provider => diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs index b67c1473..478ec315 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Logging/SerilogLoggerFactory.cs @@ -1,7 +1,6 @@ using Elastic.CommonSchema.Serilog; 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 Serilog; using Serilog.Core; using Serilog.Events; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs index 496c6a96..883de767 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Builders/RadiusPacketBuilder.cs @@ -1,6 +1,5 @@ using System.Net; using System.Text; -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.Configurations.Models.Dictionary; diff --git a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs index 0dc9abb3..cc1f702d 100644 --- a/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs +++ b/src/Multifactor.Radius.Adapter.v2.Infrastructure/Radius/Services/RadiusReplyAttributeService.cs @@ -3,7 +3,6 @@ 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.Infrastructure.Configurations.Models; using Multifactor.Radius.Adapter.v2.Shared.Extensions; namespace Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Services; 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 index b7472fd7..65b8a838 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/LdapFirstFactorProcessorTests.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Moq; using Multifactor.Core.Ldap.Schema; -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; 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 index e4014e22..a629b517 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/FirstFactor/NoneFirstFactorProcessorTests.cs @@ -1,6 +1,5 @@ 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.FirstFactor; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs index b258fd33..d7cf1c7a 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineProviderTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Moq; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs index 77606326..057b871b 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/RadiusPipelineTests.cs @@ -1,6 +1,5 @@ using System.Net; using Moq; -using Multifactor.Radius.Adapter.v2.Application.Configuration.Models; 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; 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 index 23361e1c..3326d0c8 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessChallengeStepTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Moq; -using Multifactor.Radius.Adapter.v2.Application.Configuration.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; 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 index e9f9f318..bf2094e3 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/AccessRequestFilteringStepTests.cs @@ -1,7 +1,6 @@ using System.Net; 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.Steps; using Multifactor.Radius.Adapter.v2.Application.Features.Radius.Models; 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 index 8ac3efb7..3bd2bbe5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/IpWhiteListStepTests.cs @@ -1,7 +1,6 @@ using System.Net; 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; 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 index 649bc762..d68520d5 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/LdapSchemaLoadingStepTests.cs @@ -2,7 +2,6 @@ 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.Features.Ldap.Models; using Multifactor.Radius.Adapter.v2.Application.Features.Ldap.Ports; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; 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 index dfc3dab5..32e45cfe 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/PreAuthPostCheckTests.cs @@ -1,6 +1,5 @@ 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; 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 index afb89db9..e922fadc 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Pipeline/Steps/SecondFactorStepTests.cs @@ -2,7 +2,6 @@ using Moq; using Multifactor.Core.Ldap.Name; 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; diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs index c6bd7bce..7c154317 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Application/Radius/RadiusPacketProcessorTests.cs @@ -1,6 +1,5 @@ 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.Interfaces; using Multifactor.Radius.Adapter.v2.Application.Features.Pipeline.Models; diff --git a/src/Multifactor.Radius.Adapter.v2/Program.cs b/src/Multifactor.Radius.Adapter.v2/Program.cs index c49aa90a..0cc3e28d 100644 --- a/src/Multifactor.Radius.Adapter.v2/Program.cs +++ b/src/Multifactor.Radius.Adapter.v2/Program.cs @@ -4,7 +4,6 @@ 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.Configurations.Exceptions; using Multifactor.Radius.Adapter.v2.Infrastructure.Logging; using Multifactor.Radius.Adapter.v2.Infrastructure.Radius.Sender; using Multifactor.Radius.Adapter.v2.Server;