Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Improve the Base64Provider resilience to malformed configuration files #1081

Merged
merged 5 commits into from
Jan 1, 2024
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<Product>Testcontainers</Product>
<Copyright>Copyright (c) 2019 - 2023 Andre Hofmeister and other authors</Copyright>
<Copyright>Copyright (c) 2019 - 2024 Andre Hofmeister and other authors</Copyright>
<Authors>Andre Hofmeister and contributors</Authors>
<Company>Andre Hofmeister</Company>
<Description>Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions.</Description>
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2019 - 2023 Andre Hofmeister and other authors
Copyright (c) 2019 - 2024 Andre Hofmeister and other authors

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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ See [LICENSE](https://github.com/testcontainers/testcontainers-dotnet/blob/main/

## Copyright

Copyright (c) 2019 - 2023 Andre Hofmeister and other authors.
Copyright (c) 2019 - 2024 Andre Hofmeister and other authors.

See [contributors][testcontainers-dotnet-contributors] for all contributors.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-do

## Copyright

Copyright (c) 2019 - 2023 Andre Hofmeister and other authors.
Copyright (c) 2019 - 2024 Andre Hofmeister and other authors.

See [contributors][testcontainers-dotnet-contributors] for all contributors.

Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.Papercut/Testcontainers.Papercut.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Copyright>Copyright (c) 2019 - 2023 Liam Wilson, Andre Hofmeister and other authors</Copyright>
<Copyright>Copyright (c) 2019 - 2024 Liam Wilson, Andre Hofmeister and other authors</Copyright>
<Authors>Liam Wilson, Andre Hofmeister and contributors</Authors>
<Description>A Testcontainers Papercut module for testing SMTP clients and sending emails.</Description>
</PropertyGroup>
Expand Down
27 changes: 24 additions & 3 deletions src/Testcontainers/Builders/Base64Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,37 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname)
return null;
}

if (string.IsNullOrEmpty(auth.GetString()))
if (!JsonValueKind.String.Equals(auth.ValueKind) && !JsonValueKind.Null.Equals(auth.ValueKind))
{
_logger.DockerRegistryAuthPropertyValueKindInvalid(hostname, auth.ValueKind);
return null;
}

var credentialInBytes = Convert.FromBase64String(auth.GetString());
var credential = Encoding.UTF8.GetString(credentialInBytes).Split(new[] { ':' }, 2);
var authValue = auth.GetString();

if (string.IsNullOrEmpty(authValue))
{
_logger.DockerRegistryAuthPropertyValueNotFound(hostname);
return null;
}

byte[] credentialInBytes;

try
{
credentialInBytes = Convert.FromBase64String(authValue);
}
catch (FormatException e)
{
_logger.DockerRegistryAuthPropertyValueInvalidBase64(hostname, e);
return null;
}

var credential = Encoding.Default.GetString(credentialInBytes).Split(new[] { ':' }, 2);

if (credential.Length != 2)
{
_logger.DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(hostname);
return null;
}

Expand Down
33 changes: 33 additions & 0 deletions src/Testcontainers/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.RegularExpressions;
using DotNet.Testcontainers.Images;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -82,6 +83,18 @@ private static readonly Action<ILogger, string, Exception> _DockerConfigFileNotF
private static readonly Action<ILogger, string, Exception> _SearchingDockerRegistryCredential
= LoggerMessage.Define<string>(LogLevel.Information, default, "Searching Docker registry credential in {CredentialStore}");

private static readonly Action<ILogger, string, JsonValueKind, Exception> _DockerRegistryAuthPropertyValueKindInvalid
= LoggerMessage.Define<string, JsonValueKind>(LogLevel.Warning, default, "The \"auth\" property value kind for {DockerRegistry} is invalid: {ValueKind}");

private static readonly Action<ILogger, string, Exception> _DockerRegistryAuthPropertyValueNotFound
= LoggerMessage.Define<string>(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} not found");

private static readonly Action<ILogger, string, Exception> _DockerRegistryAuthPropertyValueInvalidBase64
= LoggerMessage.Define<string>(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} is not a valid Base64 string");

private static readonly Action<ILogger, string, Exception> _DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat
= LoggerMessage.Define<string>(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} should contain one colon separating the username and the password (basic authentication)");

private static readonly Action<ILogger, string, Exception> _DockerRegistryCredentialNotFound
= LoggerMessage.Define<string>(LogLevel.Information, default, "Docker registry credential {DockerRegistry} not found");

Expand Down Expand Up @@ -212,6 +225,26 @@ public static void SearchingDockerRegistryCredential(this ILogger logger, string
_SearchingDockerRegistryCredential(logger, credentialStore, null);
}

public static void DockerRegistryAuthPropertyValueKindInvalid(this ILogger logger, string dockerRegistry, JsonValueKind valueKind)
{
_DockerRegistryAuthPropertyValueKindInvalid(logger, dockerRegistry, valueKind, null);
}

public static void DockerRegistryAuthPropertyValueNotFound(this ILogger logger, string dockerRegistry)
{
_DockerRegistryAuthPropertyValueNotFound(logger, dockerRegistry, null);
}

public static void DockerRegistryAuthPropertyValueInvalidBase64(this ILogger logger, string dockerRegistry, Exception e)
{
_DockerRegistryAuthPropertyValueInvalidBase64(logger, dockerRegistry, e);
}

public static void DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(this ILogger logger, string dockerRegistry)
{
_DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(logger, dockerRegistry, null);
}

public static void DockerRegistryCredentialNotFound(this ILogger logger, string dockerRegistry)
{
_DockerRegistryCredentialNotFound(logger, dockerRegistry, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
namespace DotNet.Testcontainers.Tests.Unit
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Images;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

Expand Down Expand Up @@ -62,6 +64,8 @@ public void ShouldGetDefaultDockerRegistryAuthenticationConfiguration()

public sealed class Base64ProviderTest
{
private readonly WarnLogger _warnLogger = new WarnLogger();

[Theory]
[InlineData("{\"auths\":{\"ghcr.io\":{}}}")]
[InlineData("{\"auths\":{\"://ghcr.io\":{}}}")]
Expand All @@ -79,26 +83,37 @@ public void ResolvePartialDockerRegistry(string jsonDocument)
}

[Theory]
[InlineData("{}", false)]
[InlineData("{\"auths\":null}", false)]
[InlineData("{\"auths\":{}}", false)]
[InlineData("{\"auths\":{\"ghcr.io\":{}}}", false)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true)]
public void ShouldGetNull(string jsonDocument, bool isApplicable)
[InlineData("{}", false, null)]
[InlineData("{\"auths\":null}", false, null)]
[InlineData("{\"auths\":{}}", false, null)]
[InlineData("{\"auths\":{\"ghcr.io\":{}}}", false, null)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true, null)]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ not found")]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ not found")]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":{}}}}", true, "The \"auth\" property value kind for https://index.docker.io/v1/ is invalid: Object")]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"Not_Base64_encoded\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ is not a valid Base64 string")]
[InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true, "The \"auth\" property value for https://index.docker.io/v1/ should contain one colon separating the username and the password (basic authentication)")]
public void ShouldGetNull(string jsonDocument, bool isApplicable, string logMessage)
{
// Given
var jsonElement = JsonDocument.Parse(jsonDocument).RootElement;

// When
var authenticationProvider = new Base64Provider(jsonElement, NullLogger.Instance);
var authenticationProvider = new Base64Provider(jsonElement, _warnLogger);
var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry);

// Then
Assert.Equal(isApplicable, authenticationProvider.IsApplicable(DockerRegistry));
Assert.Null(authConfig);

if (string.IsNullOrEmpty(logMessage))
{
Assert.Empty(_warnLogger.LogMessages);
}
else
{
Assert.Single(_warnLogger.LogMessages, item => logMessage.Equals(item.Item2));
}
}

[Fact]
Expand Down Expand Up @@ -223,5 +238,48 @@ static SetEnvVarPath()
.Distinct()));
}
}

private sealed class Disposable : IDisposable
{
static Disposable()
{
}

private Disposable()
{
}

public static IDisposable Empty { get; }
= new Disposable();

public void Dispose()
{
}
}

private sealed class WarnLogger : ILogger
{
private readonly List<Tuple<LogLevel, string>> _logMessages = new List<Tuple<LogLevel, string>>();

public IEnumerable<Tuple<LogLevel, string>> LogMessages => _logMessages;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (IsEnabled(logLevel))
{
_logMessages.Add(Tuple.Create(logLevel, formatter.Invoke(state, exception)));
}
}

public bool IsEnabled(LogLevel logLevel)
{
return LogLevel.Warning.Equals(logLevel);
}

public IDisposable BeginScope<TState>(TState state)
{
return Disposable.Empty;
}
}
}
}