Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace SecurityService.IntegrationTesting.Helpers;

public sealed class IntegrationTestCertificate : IDisposable
{
private const string PasswordValue = "password";
private const string ServerCertificateFileName = "aspnetapp-web-api.pfx";
private const string RootCertificateFileName = "aspnetapp-root-cert.cer";
private const string ContainerCertificateDirectoryPath = "/app/test-certs";
private const string RootSubjectName = "CN=SecurityServiceIntegrationTestRoot";
private const string LocalhostName = "localhost";
private const string LoopbackAddress = "127.0.0.1";

private readonly string _certificateDirectory;
private readonly X509Certificate2 _certificate;
private bool _trustedRootInstalled;
private bool _disposed;

private IntegrationTestCertificate(string certificateDirectory, X509Certificate2 certificate)
{
_certificateDirectory = certificateDirectory;
_certificate = certificate;
}

public string CertificateDirectory => _certificateDirectory;

public string ContainerCertificateDirectory => ContainerCertificateDirectoryPath;

public string Password => PasswordValue;

public string RootCertificatePath => Path.Combine(_certificateDirectory, RootCertificateFileName);

public string ServerCertificatePath => Path.Combine(_certificateDirectory, ServerCertificateFileName);

public string Thumbprint => _certificate.Thumbprint ?? string.Empty;

public static IntegrationTestCertificate Create()
{
string certificateDirectory = Path.Combine(Path.GetTempPath(), $"securityservice-cert-{Guid.NewGuid():N}");
Directory.CreateDirectory(certificateDirectory);

using RSA rsa = RSA.Create(2048);
CertificateRequest request = new CertificateRequest(RootSubjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));

OidCollection enhancedKeyUsages = new OidCollection();
enhancedKeyUsages.Add(new Oid("1.3.6.1.5.5.7.3.1"));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhancedKeyUsages, false));

SubjectAlternativeNameBuilder subjectAlternativeNames = new SubjectAlternativeNameBuilder();
subjectAlternativeNames.AddDnsName(LocalhostName);
subjectAlternativeNames.AddIpAddress(IPAddress.Parse(LoopbackAddress));
request.CertificateExtensions.Add(subjectAlternativeNames.Build());

request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));

using X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30));
byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, PasswordValue);
byte[] certificateBytes = certificate.Export(X509ContentType.Cert);

string serverCertificatePath = Path.Combine(certificateDirectory, ServerCertificateFileName);
string rootCertificatePath = Path.Combine(certificateDirectory, RootCertificateFileName);
File.WriteAllBytes(serverCertificatePath, pfxBytes);
File.WriteAllBytes(rootCertificatePath, certificateBytes);

var result = new IntegrationTestCertificate(certificateDirectory, X509CertificateLoader.LoadPkcs12(pfxBytes, PasswordValue, X509KeyStorageFlags.EphemeralKeySet));
result.InstallTrustedRoot();
return result;
}

public void InstallTrustedRoot()
{
ThrowIfDisposed();

using X509Store store = new X509Store(StoreName.TrustedPeople, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(_certificate);
_trustedRootInstalled = true;
}

public string GetContainerCertificatePath()
{
return $"{ContainerCertificateDirectoryPath}/{ServerCertificateFileName}";
}

public void Dispose()
{
if (_disposed)
{
return;
}

if (_trustedRootInstalled)
{
RemoveTrustedRoot();
}

if (Directory.Exists(_certificateDirectory))
{
Directory.Delete(_certificateDirectory, recursive: true);
}

_certificate.Dispose();
_disposed = true;
}

private void RemoveTrustedRoot()
{
using X509Store store = new X509Store(StoreName.TrustedPeople, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);

foreach (X509Certificate2 certificate in store.Certificates.Find(X509FindType.FindByThumbprint, Thumbprint, validOnly: false))
{
store.Remove(certificate);
}

_trustedRootInstalled = false;
}

private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,11 @@ public async Task WhenIGetTheIdentityResourcesIdentityResourceDetailsAreReturned
List<(String, String)> roleList = new List<(String, String)>();
foreach (CreateRoleRequest request in requests){
Result? result = await this.SecurityServiceClient.CreateRole(request, cancellationToken).ConfigureAwait(false);
result.IsSuccess.ShouldBeTrue();
result.IsSuccess.ShouldBeTrue(result.Message);
}

Result<List<RoleResponse>>? roles = await this.SecurityServiceClient.GetRoles(cancellationToken);
roles.IsSuccess.ShouldBeTrue();
roles.IsSuccess.ShouldBeTrue(roles.Message);

foreach (CreateRoleRequest request in requests) {
RoleResponse r = roles.Data.Single(r => r.RoleName == request.RoleName);
Expand Down
144 changes: 88 additions & 56 deletions SecurityService.IntegrationTests/Common/DockerHelper.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
using System;
using System.Collections.Generic;
using System.Text;
#nullable enable
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using Shared.Serialisation;

namespace SecurityService.IntergrationTests.Common
{
using Client;
using Ductus.FluentDocker;
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.Commands;
using Ductus.FluentDocker.Common;
using Ductus.FluentDocker.Model.Builders;
using Ductus.FluentDocker.Services;
using Ductus.FluentDocker.Services.Extensions;
using Microsoft.EntityFrameworkCore;
using SecurityService.IntegrationTesting.Helpers;
using Shared.IntegrationTesting;
using System.Data.SqlClient;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

public class DockerHelper : Shared.IntegrationTesting.TestContainers.DockerHelper
{
public ISecurityServiceClient SecurityServiceClient;
public ISecurityServiceClient? SecurityServiceClient;
private IntegrationTestCertificate? _integrationTestCertificate;

String Serialise(Object arg)
{
Expand All @@ -38,71 +30,111 @@ Object Deserialise(String arg, Type type)
return StringSerialiser.DeserializeObject<Object>(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase));
}

public async Task StartContainersForScenarioRun(String scenarioName, DockerServices dockerServices)
public override async Task StartContainersForScenarioRun(String scenarioName, DockerServices dockerServices)
{
await base.StartContainersForScenarioRun(scenarioName, dockerServices);

Func<String, String> securityServiceBaseAddressResolver = api => $"https://localhost:{this.SecurityServicePort}";
HttpClient httpClient = new HttpClient();
this.SecurityServiceClient = new SecurityServiceClient(securityServiceBaseAddressResolver,httpClient, Serialise, Deserialise);
this._integrationTestCertificate = IntegrationTestCertificate.Create();

try
{
await base.StartContainersForScenarioRun(scenarioName, dockerServices);

Func<String, String> securityServiceBaseAddressResolver = _ => $"https://localhost:{this.SecurityServicePort}";
HttpClient httpClient = this.CreatePinnedHttpClient();
this.SecurityServiceClient = new SecurityServiceClient(securityServiceBaseAddressResolver, httpClient, Serialise, Deserialise);

ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault;
ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault;
}
catch
{
this.DisposeIntegrationTestCertificate();
throw;
}
}

/*public override DotNet.Testcontainers.Builders.ContainerBuilder SetupSecurityServiceContainer()
public override ContainerBuilder SetupSecurityServiceContainer()
{

this.Trace("About to Start Security Container");

List<String> environmentVariables = this.GetCommonEnvironmentVariables();
environmentVariables.Add($"ServiceOptions:PublicOrigin=https://{this.SecurityServiceContainerName}:{DockerPorts.SecurityServiceDockerPort}");
environmentVariables.Add($"ServiceOptions:IssuerUrl=https://{this.SecurityServiceContainerName}:{DockerPorts.SecurityServiceDockerPort}");
environmentVariables.Add("ASPNETCORE_ENVIRONMENT=IntegrationTest");
environmentVariables.Add($"urls=https://*:{DockerPorts.SecurityServiceDockerPort}");

environmentVariables.Add("ServiceOptions:PasswordOptions:RequiredLength=6");
environmentVariables.Add("ServiceOptions:PasswordOptions:RequireDigit=false");
environmentVariables.Add("ServiceOptions:PasswordOptions:RequireUpperCase=false");
environmentVariables.Add("ServiceOptions:UserOptions:RequireUniqueEmail=false");
environmentVariables.Add("ServiceOptions:SignInOptions:RequireConfirmedEmail=false");

environmentVariables.Add(this.SetConnectionString("ConnectionStrings:PersistedGrantDbContext", $"PersistedGrantStore-{this.TestId}", this.UseSecureSqlServerDatabase));
environmentVariables.Add(this.SetConnectionString("ConnectionStrings:ConfigurationDbContext", $"Configuration-{this.TestId}", this.UseSecureSqlServerDatabase));
environmentVariables.Add(this.SetConnectionString("ConnectionStrings:AuthenticationDbContext", $"Authentication-{this.TestId}", this.UseSecureSqlServerDatabase));
if (this._integrationTestCertificate is null)
{
throw new InvalidOperationException("Integration test certificate was not initialised before container setup.");
}

List<String> additionalEnvironmentVariables = this.GetAdditionalVariables(ContainerType.SecurityService);
Dictionary<String, String> environmentVariables = this.GetCommonEnvironmentVariables();
environmentVariables.Add("ServiceOptions:PublicOrigin", $"https://{this.SecurityServiceContainerName}:{DockerPorts.SecurityServiceDockerPort}");
environmentVariables.Add("ServiceOptions:IssuerUrl", $"https://{this.SecurityServiceContainerName}:{DockerPorts.SecurityServiceDockerPort}");
environmentVariables.Add("ASPNETCORE_ENVIRONMENT", "IntegrationTest");
environmentVariables.Add("urls", $"https://*:{DockerPorts.SecurityServiceDockerPort}");
environmentVariables.Add("ServiceOptions:KestrelOptions:Path", this._integrationTestCertificate.GetContainerCertificatePath());
environmentVariables.Add("ServiceOptions:KestrelOptions:Password", this._integrationTestCertificate.Password);
environmentVariables.Add("ServiceOptions:PasswordOptions:RequiredLength", "6");
environmentVariables.Add("ServiceOptions:PasswordOptions:RequireDigit", "false");
environmentVariables.Add("ServiceOptions:PasswordOptions:RequireUpperCase", "false");
environmentVariables.Add("ServiceOptions:UserOptions:RequireUniqueEmail", "false");
environmentVariables.Add("ServiceOptions:SignInOptions:RequireConfirmedEmail", "false");
environmentVariables.Add("ConnectionStrings:PersistedGrantDbContext", this.SetConnectionString($"PersistedGrantStore-{this.TestId}", this.UseSecureSqlServerDatabase));
environmentVariables.Add("ConnectionStrings:ConfigurationDbContext", this.SetConnectionString($"Configuration-{this.TestId}", this.UseSecureSqlServerDatabase));
environmentVariables.Add("ConnectionStrings:AuthenticationDbContext", this.SetConnectionString($"Authentication-{this.TestId}", this.UseSecureSqlServerDatabase));

Dictionary<String, String> additionalEnvironmentVariables = this.GetAdditionalVariables(ContainerType.SecurityService);
if (additionalEnvironmentVariables != null)
{
environmentVariables.AddRange(additionalEnvironmentVariables);
foreach (KeyValuePair<String, String> additionalEnvironmentVariable in additionalEnvironmentVariables)
{
environmentVariables.Add(additionalEnvironmentVariable.Key, additionalEnvironmentVariable.Value);
}
}

SimpleResults.Result<(String imageName, Boolean useLatest)> imageDetailsResult = this.GetImageDetails(ContainerType.SecurityService);
if (imageDetailsResult.IsFailed)
{
throw new Exception($"Image details not found for {ContainerType.SecurityService}");
}

ContainerBuilder securityServiceContainer = new Builder().UseContainer().WithName(this.SecurityServiceContainerName)
.WithEnvironment(environmentVariables.ToArray())
.UseImageDetails(imageDetailsResult.Data)
.MountHostFolder(this.DockerPlatform, this.HostTraceFolder)
.SetDockerCredentials(this.DockerCredentials);
ContainerBuilder securityServiceContainer = new ContainerBuilder(imageDetailsResult.Data.imageName)
.WithName(this.SecurityServiceContainerName)
.WithEnvironment(environmentVariables)
.WithBindMount(this._integrationTestCertificate.CertificateDirectory, this._integrationTestCertificate.ContainerCertificateDirectory, AccessMode.ReadOnly);

Int32? hostPort = this.GetHostPort(ContainerType.SecurityService);
if (hostPort == null)
if (hostPort is null || hostPort <= 0)
{
securityServiceContainer = securityServiceContainer.ExposePort(DockerPorts.SecurityServiceDockerPort);
securityServiceContainer = securityServiceContainer.WithPortBinding(DockerPorts.SecurityServiceDockerPort, true);
}
else
{
securityServiceContainer = securityServiceContainer.ExposePort(hostPort.Value, DockerPorts.SecurityServiceDockerPort);
securityServiceContainer = securityServiceContainer.WithPortBinding(DockerPorts.SecurityServiceDockerPort, hostPort.Value);
}

// Now build and return the container
return securityServiceContainer;
}*/

public override async Task CreateSubscriptions(){
// No subscriptions needed
}

public override Task CreateSubscriptions()
{
return Task.CompletedTask;
}

public void DisposeIntegrationTestCertificate()
{
this._integrationTestCertificate?.Dispose();
this._integrationTestCertificate = null;
}

private HttpClient CreatePinnedHttpClient()
{
if (this._integrationTestCertificate is null)
{
throw new InvalidOperationException("Integration test certificate was not initialised before HttpClient creation.");
}

HttpClientHandler handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
certificate is X509Certificate2 certificate2 &&
String.Equals(certificate2.Thumbprint, this._integrationTestCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase)
};

return new HttpClient(handler, disposeHandler: true);
}
}
}
9 changes: 8 additions & 1 deletion SecurityService.IntegrationTests/Common/GenericSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,14 @@ public async Task StartSystem()
public async Task StopSystem(){
DockerServices sharedDockerServices = DockerServices.SqlServer;
this.TestingContext.Logger.LogInformation("About to Stop Containers for Scenario Run");
await this.TestingContext.DockerHelper.StopContainersForScenarioRun(sharedDockerServices).ConfigureAwait(false);
try
{
await this.TestingContext.DockerHelper.StopContainersForScenarioRun(sharedDockerServices).ConfigureAwait(false);
}
finally
{
this.TestingContext.DockerHelper.DisposeIntegrationTestCertificate();
}
this.TestingContext.Logger.LogInformation("Containers for Scenario Run Stopped");
}
}
Expand Down
Loading
Loading