From 7d6fd67b6c23bb0026211b3fec6e57722cb26eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Thu, 28 Dec 2023 13:20:46 +0100 Subject: [PATCH 1/5] chore: Improve the Base64Provider resilience to malformed configuration files Also add some tests to cover different pathological scenarios. --- src/Testcontainers/Builders/Base64Provider.cs | 25 ++++++- .../Unit/Builders/Base64ProviderTest.cs | 65 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs diff --git a/src/Testcontainers/Builders/Base64Provider.cs b/src/Testcontainers/Builders/Base64Provider.cs index fe71dddba..607afb1c6 100644 --- a/src/Testcontainers/Builders/Base64Provider.cs +++ b/src/Testcontainers/Builders/Base64Provider.cs @@ -72,12 +72,20 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - if (string.IsNullOrEmpty(auth.GetString())) + var authValue = JsonValueKind.String.Equals(auth.ValueKind) ? auth.GetString() : null; + + if (string.IsNullOrEmpty(authValue)) + { + return null; + } + + var credentialInBytes = DecodeBase64String(authValue); + + if (credentialInBytes == null) { return null; } - var credentialInBytes = Convert.FromBase64String(auth.GetString()); var credential = Encoding.UTF8.GetString(credentialInBytes).Split(new[] { ':' }, 2); if (credential.Length != 2) @@ -88,5 +96,18 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) _logger.DockerRegistryCredentialFound(hostname); return new DockerRegistryAuthenticationConfiguration(authProperty.Name, credential[0], credential[1]); } + + [CanBeNull] + private static byte[] DecodeBase64String(string base64) + { + try + { + return Convert.FromBase64String(base64); + } + catch (FormatException) + { + return null; + } + } } } diff --git a/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs b/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs new file mode 100644 index 000000000..c8d016f0d --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using DotNet.Testcontainers.Builders; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace DotNet.Testcontainers.Tests.Unit +{ + public class Base64ProviderTest + { + [Fact] + public void ShouldDecodeAuth() + { + // Given + // lang=json + const string config = """ + { + "auths": { + "https://index.docker.io/v1/": { + "auth": "dXNlcjpwYXNzd29yZA==" + } + } + } + """; + var provider = new Base64Provider(JsonDocument.Parse(config), NullLogger.Instance); + + // When + var authConfig = provider.GetAuthConfig("https://index.docker.io/v1/"); + + // Then + Assert.NotNull(authConfig); + Assert.Equal("https://index.docker.io/v1/", authConfig.RegistryEndpoint); + Assert.Equal("user", authConfig.Username); + Assert.Equal("password", authConfig.Password); + Assert.Null(authConfig.IdentityToken); + } + + [Theory] + [InlineData("{}")] + [InlineData("null")] + [InlineData("\"not base 64\"")] + [InlineData("\"ww==\"")] // invalid UTF8 (single 0xC3 byte) + [InlineData("\"Xw==\"")] // valid base 64 but contains no colon separator + public void ShouldNotDecodeAuth(string auth) + { + // Given + // lang=json + var config = $$""" + { + "auths": { + "https://index.docker.io/v1/": { + "auth": {{auth}} + } + } + } + """; + var provider = new Base64Provider(JsonDocument.Parse(config), NullLogger.Instance); + + // When + var authConfig = provider.GetAuthConfig("https://index.docker.io/v1/"); + + // Then + Assert.Null(authConfig); + } + } +} From f79827f2ba376b4e29be8327d08f89961252442f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Thu, 28 Dec 2023 20:03:55 +0100 Subject: [PATCH 2/5] Log warnings if the "auth" configuration can't be decoded --- src/Testcontainers/Builders/Base64Provider.cs | 13 ++++- src/Testcontainers/Logging.cs | 33 +++++++++++++ .../Unit/Builders/Base64ProviderTest.cs | 47 ++++++++++++++----- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/Testcontainers/Builders/Base64Provider.cs b/src/Testcontainers/Builders/Base64Provider.cs index 607afb1c6..86926beed 100644 --- a/src/Testcontainers/Builders/Base64Provider.cs +++ b/src/Testcontainers/Builders/Base64Provider.cs @@ -72,10 +72,19 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - var authValue = JsonValueKind.String.Equals(auth.ValueKind) ? auth.GetString() : null; + var isValidKind = JsonValueKind.String.Equals(auth.ValueKind) || JsonValueKind.Null.Equals(auth.ValueKind); + var authValue = isValidKind ? auth.GetString() : null; if (string.IsNullOrEmpty(authValue)) { + if (isValidKind) + { + _logger.DockerRegistryCredentialMissingAuth(hostname); + } + else + { + _logger.DockerRegistryCredentialInvalidAuth(hostname, auth.ValueKind); + } return null; } @@ -83,6 +92,7 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) if (credentialInBytes == null) { + _logger.DockerRegistryCredentialInvalidEncodedBase64(hostname); return null; } @@ -90,6 +100,7 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) if (credential.Length != 2) { + _logger.DockerRegistryCredentialInvalidDecodedBase64(hostname); return null; } diff --git a/src/Testcontainers/Logging.cs b/src/Testcontainers/Logging.cs index c9bfddf90..7bfe75b9a 100644 --- a/src/Testcontainers/Logging.cs +++ b/src/Testcontainers/Logging.cs @@ -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; @@ -82,6 +83,18 @@ private static readonly Action _DockerConfigFileNotF private static readonly Action _SearchingDockerRegistryCredential = LoggerMessage.Define(LogLevel.Information, default, "Searching Docker registry credential in {CredentialStore}"); + private static readonly Action _DockerRegistryCredentialMissingAuth + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is missing"); + + private static readonly Action _DockerRegistryCredentialInvalidAuth + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is invalid ({ValueKind} instead of String)"); + + private static readonly Action _DockerRegistryCredentialInvalidEncodedBase64 + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is not a valid base64 string"); + + private static readonly Action _DockerRegistryCredentialInvalidDecodedBase64 + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry}, once base64 decoded, should contain one and only one colon separating the user name and password"); + private static readonly Action _DockerRegistryCredentialNotFound = LoggerMessage.Define(LogLevel.Information, default, "Docker registry credential {DockerRegistry} not found"); @@ -212,6 +225,26 @@ public static void SearchingDockerRegistryCredential(this ILogger logger, string _SearchingDockerRegistryCredential(logger, credentialStore, null); } + public static void DockerRegistryCredentialMissingAuth(this ILogger logger, string dockerRegistry) + { + _DockerRegistryCredentialMissingAuth(logger, dockerRegistry, null); + } + + public static void DockerRegistryCredentialInvalidAuth(this ILogger logger, string dockerRegistry, JsonValueKind kind) + { + _DockerRegistryCredentialInvalidAuth(logger, dockerRegistry, kind, null); + } + + public static void DockerRegistryCredentialInvalidEncodedBase64(this ILogger logger, string dockerRegistry) + { + _DockerRegistryCredentialInvalidEncodedBase64(logger, dockerRegistry, null); + } + + public static void DockerRegistryCredentialInvalidDecodedBase64(this ILogger logger, string dockerRegistry) + { + _DockerRegistryCredentialInvalidDecodedBase64(logger, dockerRegistry, null); + } + public static void DockerRegistryCredentialNotFound(this ILogger logger, string dockerRegistry) { _DockerRegistryCredentialNotFound(logger, dockerRegistry, null); diff --git a/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs b/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs index c8d016f0d..2297de5e7 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs @@ -1,10 +1,14 @@ -using System.Text.Json; -using DotNet.Testcontainers.Builders; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit; - namespace DotNet.Testcontainers.Tests.Unit { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json; + using DotNet.Testcontainers.Builders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using Xunit; + public class Base64ProviderTest { [Fact] @@ -35,12 +39,13 @@ public void ShouldDecodeAuth() } [Theory] - [InlineData("{}")] - [InlineData("null")] - [InlineData("\"not base 64\"")] - [InlineData("\"ww==\"")] // invalid UTF8 (single 0xC3 byte) - [InlineData("\"Xw==\"")] // valid base 64 but contains no colon separator - public void ShouldNotDecodeAuth(string auth) + [InlineData("null", "The \"auth\" value for https://index.docker.io/v1/ is missing")] + [InlineData("\"\"", "The \"auth\" value for https://index.docker.io/v1/ is missing")] + [InlineData("{}", "The \"auth\" value for https://index.docker.io/v1/ is invalid (Object instead of String)")] + [InlineData("\"not base 64\"", "The \"auth\" value for https://index.docker.io/v1/ is not a valid base64 string")] + [InlineData("\"ww==\"", "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and password")] // invalid UTF8 (single 0xC3 byte) + [InlineData("\"Xw==\"", "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and password")] // valid base 64 but contains no colon separator + public void ShouldNotDecodeAuth(string auth, string expectedMessage) { // Given // lang=json @@ -53,13 +58,31 @@ public void ShouldNotDecodeAuth(string auth) } } """; - var provider = new Base64Provider(JsonDocument.Parse(config), NullLogger.Instance); + var recorder = new LogRecorder(); + var provider = new Base64Provider(JsonDocument.Parse(config), recorder); // When var authConfig = provider.GetAuthConfig("https://index.docker.io/v1/"); // Then Assert.Null(authConfig); + Assert.Equal(expectedMessage, Assert.Single(recorder.Logs.Where(e => e.Level == LogLevel.Warning).Select(e => e.Text))); + } + + private class LogRecorder : ILogger + { + private readonly List<(LogLevel Level, string Text)> _logs = new List<(LogLevel Level, string Text)>(); + + public IEnumerable<(LogLevel Level, string Text)> Logs => _logs; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _logs.Add((logLevel, formatter(state, exception))); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) => null; } } } From acd009ecea51ea89c9cbd232006a6bd426cd730c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Thu, 28 Dec 2023 20:24:30 +0100 Subject: [PATCH 3/5] Move the Base64Provider tests in the existing test class --- src/Testcontainers/Logging.cs | 2 +- .../Unit/Builders/Base64ProviderTest.cs | 88 ------------------- ...ockerRegistryAuthenticationProviderTest.cs | 49 ++++++++--- 3 files changed, 40 insertions(+), 99 deletions(-) delete mode 100644 tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs diff --git a/src/Testcontainers/Logging.cs b/src/Testcontainers/Logging.cs index 7bfe75b9a..d162f96e6 100644 --- a/src/Testcontainers/Logging.cs +++ b/src/Testcontainers/Logging.cs @@ -93,7 +93,7 @@ private static readonly Action _DockerRegistryCreden = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is not a valid base64 string"); private static readonly Action _DockerRegistryCredentialInvalidDecodedBase64 - = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry}, once base64 decoded, should contain one and only one colon separating the user name and password"); + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry}, once base64 decoded, should contain one and only one colon separating the user name and the password"); private static readonly Action _DockerRegistryCredentialNotFound = LoggerMessage.Define(LogLevel.Information, default, "Docker registry credential {DockerRegistry} not found"); diff --git a/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs b/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs deleted file mode 100644 index 2297de5e7..000000000 --- a/tests/Testcontainers.Tests/Unit/Builders/Base64ProviderTest.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace DotNet.Testcontainers.Tests.Unit -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.Json; - using DotNet.Testcontainers.Builders; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.Abstractions; - using Xunit; - - public class Base64ProviderTest - { - [Fact] - public void ShouldDecodeAuth() - { - // Given - // lang=json - const string config = """ - { - "auths": { - "https://index.docker.io/v1/": { - "auth": "dXNlcjpwYXNzd29yZA==" - } - } - } - """; - var provider = new Base64Provider(JsonDocument.Parse(config), NullLogger.Instance); - - // When - var authConfig = provider.GetAuthConfig("https://index.docker.io/v1/"); - - // Then - Assert.NotNull(authConfig); - Assert.Equal("https://index.docker.io/v1/", authConfig.RegistryEndpoint); - Assert.Equal("user", authConfig.Username); - Assert.Equal("password", authConfig.Password); - Assert.Null(authConfig.IdentityToken); - } - - [Theory] - [InlineData("null", "The \"auth\" value for https://index.docker.io/v1/ is missing")] - [InlineData("\"\"", "The \"auth\" value for https://index.docker.io/v1/ is missing")] - [InlineData("{}", "The \"auth\" value for https://index.docker.io/v1/ is invalid (Object instead of String)")] - [InlineData("\"not base 64\"", "The \"auth\" value for https://index.docker.io/v1/ is not a valid base64 string")] - [InlineData("\"ww==\"", "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and password")] // invalid UTF8 (single 0xC3 byte) - [InlineData("\"Xw==\"", "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and password")] // valid base 64 but contains no colon separator - public void ShouldNotDecodeAuth(string auth, string expectedMessage) - { - // Given - // lang=json - var config = $$""" - { - "auths": { - "https://index.docker.io/v1/": { - "auth": {{auth}} - } - } - } - """; - var recorder = new LogRecorder(); - var provider = new Base64Provider(JsonDocument.Parse(config), recorder); - - // When - var authConfig = provider.GetAuthConfig("https://index.docker.io/v1/"); - - // Then - Assert.Null(authConfig); - Assert.Equal(expectedMessage, Assert.Single(recorder.Logs.Where(e => e.Level == LogLevel.Warning).Select(e => e.Text))); - } - - private class LogRecorder : ILogger - { - private readonly List<(LogLevel Level, string Text)> _logs = new List<(LogLevel Level, string Text)>(); - - public IEnumerable<(LogLevel Level, string Text)> Logs => _logs; - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - _logs.Add((logLevel, formatter(state, exception))); - } - - public bool IsEnabled(LogLevel logLevel) => true; - - public IDisposable BeginScope(TState state) => null; - } - } -} diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index 11a080559..697130c1a 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -1,6 +1,7 @@ namespace DotNet.Testcontainers.Tests.Unit { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -8,6 +9,7 @@ namespace DotNet.Testcontainers.Tests.Unit using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Images; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -79,26 +81,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\" value for https://index.docker.io/v1/ is missing")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is missing")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":{}}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is invalid (Object instead of String)")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"not base64\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is not a valid base64 string")] + [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and the password")] + public void ShouldGetNull(string jsonDocument, bool isApplicable, string warning) { // Given var jsonElement = JsonDocument.Parse(jsonDocument).RootElement; + var recorder = new LogRecorder(); // When - var authenticationProvider = new Base64Provider(jsonElement, NullLogger.Instance); + var authenticationProvider = new Base64Provider(jsonElement, recorder); var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry); // Then Assert.Equal(isApplicable, authenticationProvider.IsApplicable(DockerRegistry)); Assert.Null(authConfig); + if (warning == null) + { + Assert.Empty(recorder.Logs.Where(e => e.Level == LogLevel.Warning)); + } + else + { + Assert.Equal(warning, Assert.Single(recorder.Logs.Where(e => e.Level == LogLevel.Warning).Select(e => e.Text))); + } } [Fact] @@ -119,6 +132,22 @@ public void ShouldGetAuthConfig() Assert.Equal("username", authConfig.Username); Assert.Equal("password", authConfig.Password); } + + private class LogRecorder : ILogger + { + private readonly List<(LogLevel Level, string Text)> _logs = new List<(LogLevel Level, string Text)>(); + + public IEnumerable<(LogLevel Level, string Text)> Logs => _logs; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _logs.Add((logLevel, formatter(state, exception))); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) => null; + } } public sealed class CredsStoreProviderTest : SetEnvVarPath From 172c3f50d3a3916dc231bc3d42901c5e9905337c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:02:55 +0100 Subject: [PATCH 4/5] chore: Reduce nesting --- src/Testcontainers/Builders/Base64Provider.cs | 45 ++++------ src/Testcontainers/Logging.cs | 32 +++---- ...ockerRegistryAuthenticationProviderTest.cs | 83 +++++++++++++------ 3 files changed, 89 insertions(+), 71 deletions(-) diff --git a/src/Testcontainers/Builders/Base64Provider.cs b/src/Testcontainers/Builders/Base64Provider.cs index 86926beed..7998aa5e8 100644 --- a/src/Testcontainers/Builders/Base64Provider.cs +++ b/src/Testcontainers/Builders/Base64Provider.cs @@ -72,53 +72,42 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - var isValidKind = JsonValueKind.String.Equals(auth.ValueKind) || JsonValueKind.Null.Equals(auth.ValueKind); - var authValue = isValidKind ? auth.GetString() : null; + if (!JsonValueKind.String.Equals(auth.ValueKind) && !JsonValueKind.Null.Equals(auth.ValueKind)) + { + _logger.DockerRegistryAuthPropertyValueKindInvalid(hostname, auth.ValueKind); + return null; + } + + var authValue = auth.GetString(); if (string.IsNullOrEmpty(authValue)) { - if (isValidKind) - { - _logger.DockerRegistryCredentialMissingAuth(hostname); - } - else - { - _logger.DockerRegistryCredentialInvalidAuth(hostname, auth.ValueKind); - } + _logger.DockerRegistryAuthPropertyValueNotFound(hostname); return null; } - var credentialInBytes = DecodeBase64String(authValue); + byte[] credentialInBytes; - if (credentialInBytes == null) + try + { + credentialInBytes = Convert.FromBase64String(authValue); + } + catch (FormatException e) { - _logger.DockerRegistryCredentialInvalidEncodedBase64(hostname); + _logger.DockerRegistryAuthPropertyValueInvalidBase64(hostname, e); return null; } - var credential = Encoding.UTF8.GetString(credentialInBytes).Split(new[] { ':' }, 2); + var credential = Encoding.Default.GetString(credentialInBytes).Split(new[] { ':' }, 2); if (credential.Length != 2) { - _logger.DockerRegistryCredentialInvalidDecodedBase64(hostname); + _logger.DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(hostname); return null; } _logger.DockerRegistryCredentialFound(hostname); return new DockerRegistryAuthenticationConfiguration(authProperty.Name, credential[0], credential[1]); } - - [CanBeNull] - private static byte[] DecodeBase64String(string base64) - { - try - { - return Convert.FromBase64String(base64); - } - catch (FormatException) - { - return null; - } - } } } diff --git a/src/Testcontainers/Logging.cs b/src/Testcontainers/Logging.cs index d162f96e6..18f5d2ae0 100644 --- a/src/Testcontainers/Logging.cs +++ b/src/Testcontainers/Logging.cs @@ -83,17 +83,17 @@ private static readonly Action _DockerConfigFileNotF private static readonly Action _SearchingDockerRegistryCredential = LoggerMessage.Define(LogLevel.Information, default, "Searching Docker registry credential in {CredentialStore}"); - private static readonly Action _DockerRegistryCredentialMissingAuth - = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is missing"); + private static readonly Action _DockerRegistryAuthPropertyValueKindInvalid + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value kind for {DockerRegistry} is invalid: {ValueKind}"); - private static readonly Action _DockerRegistryCredentialInvalidAuth - = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is invalid ({ValueKind} instead of String)"); + private static readonly Action _DockerRegistryAuthPropertyValueNotFound + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} not found"); - private static readonly Action _DockerRegistryCredentialInvalidEncodedBase64 - = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry} is not a valid base64 string"); + private static readonly Action _DockerRegistryAuthPropertyValueInvalidBase64 + = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" property value for {DockerRegistry} is not a valid Base64 string"); - private static readonly Action _DockerRegistryCredentialInvalidDecodedBase64 - = LoggerMessage.Define(LogLevel.Warning, default, "The \"auth\" value for {DockerRegistry}, once base64 decoded, should contain one and only one colon separating the user name and the password"); + private static readonly Action _DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat + = LoggerMessage.Define(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 _DockerRegistryCredentialNotFound = LoggerMessage.Define(LogLevel.Information, default, "Docker registry credential {DockerRegistry} not found"); @@ -225,24 +225,24 @@ public static void SearchingDockerRegistryCredential(this ILogger logger, string _SearchingDockerRegistryCredential(logger, credentialStore, null); } - public static void DockerRegistryCredentialMissingAuth(this ILogger logger, string dockerRegistry) + public static void DockerRegistryAuthPropertyValueKindInvalid(this ILogger logger, string dockerRegistry, JsonValueKind valueKind) { - _DockerRegistryCredentialMissingAuth(logger, dockerRegistry, null); + _DockerRegistryAuthPropertyValueKindInvalid(logger, dockerRegistry, valueKind, null); } - public static void DockerRegistryCredentialInvalidAuth(this ILogger logger, string dockerRegistry, JsonValueKind kind) + public static void DockerRegistryAuthPropertyValueNotFound(this ILogger logger, string dockerRegistry) { - _DockerRegistryCredentialInvalidAuth(logger, dockerRegistry, kind, null); + _DockerRegistryAuthPropertyValueNotFound(logger, dockerRegistry, null); } - public static void DockerRegistryCredentialInvalidEncodedBase64(this ILogger logger, string dockerRegistry) + public static void DockerRegistryAuthPropertyValueInvalidBase64(this ILogger logger, string dockerRegistry, Exception e) { - _DockerRegistryCredentialInvalidEncodedBase64(logger, dockerRegistry, null); + _DockerRegistryAuthPropertyValueInvalidBase64(logger, dockerRegistry, e); } - public static void DockerRegistryCredentialInvalidDecodedBase64(this ILogger logger, string dockerRegistry) + public static void DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(this ILogger logger, string dockerRegistry) { - _DockerRegistryCredentialInvalidDecodedBase64(logger, dockerRegistry, null); + _DockerRegistryAuthPropertyValueInvalidBasicAuthenticationFormat(logger, dockerRegistry, null); } public static void DockerRegistryCredentialNotFound(this ILogger logger, string dockerRegistry) diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs index 697130c1a..27beda932 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs @@ -64,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\":{}}}")] @@ -86,31 +88,31 @@ public void ResolvePartialDockerRegistry(string jsonDocument) [InlineData("{\"auths\":{}}", false, null)] [InlineData("{\"auths\":{\"ghcr.io\":{}}}", false, null)] [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{}}}", true, null)] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":null}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is missing")] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is missing")] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":{}}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is invalid (Object instead of String)")] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"not base64\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/ is not a valid base64 string")] - [InlineData("{\"auths\":{\"" + DockerRegistry + "\":{\"auth\":\"dXNlcm5hbWU=\"}}}", true, "The \"auth\" value for https://index.docker.io/v1/, once base64 decoded, should contain one and only one colon separating the user name and the password")] - public void ShouldGetNull(string jsonDocument, bool isApplicable, string warning) + [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; - var recorder = new LogRecorder(); // When - var authenticationProvider = new Base64Provider(jsonElement, recorder); + var authenticationProvider = new Base64Provider(jsonElement, _warnLogger); var authConfig = authenticationProvider.GetAuthConfig(DockerRegistry); // Then Assert.Equal(isApplicable, authenticationProvider.IsApplicable(DockerRegistry)); Assert.Null(authConfig); - if (warning == null) + + if (string.IsNullOrEmpty(logMessage)) { - Assert.Empty(recorder.Logs.Where(e => e.Level == LogLevel.Warning)); + Assert.Empty(_warnLogger.LogMessages); } else { - Assert.Equal(warning, Assert.Single(recorder.Logs.Where(e => e.Level == LogLevel.Warning).Select(e => e.Text))); + Assert.Single(_warnLogger.LogMessages, item => logMessage.Equals(item.Item2)); } } @@ -132,22 +134,6 @@ public void ShouldGetAuthConfig() Assert.Equal("username", authConfig.Username); Assert.Equal("password", authConfig.Password); } - - private class LogRecorder : ILogger - { - private readonly List<(LogLevel Level, string Text)> _logs = new List<(LogLevel Level, string Text)>(); - - public IEnumerable<(LogLevel Level, string Text)> Logs => _logs; - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - _logs.Add((logLevel, formatter(state, exception))); - } - - public bool IsEnabled(LogLevel logLevel) => true; - - public IDisposable BeginScope(TState state) => null; - } } public sealed class CredsStoreProviderTest : SetEnvVarPath @@ -252,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> _logMessages = new List>(); + + public IEnumerable> LogMessages => _logMessages; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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 state) + { + return Disposable.Empty; + } + } } } From cfc4acb200b4349a003515b1c489ce8c9acf6503 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:03:32 +0100 Subject: [PATCH 5/5] chore: Update copyright year --- Directory.Build.props | 2 +- LICENSE | 2 +- README.md | 2 +- docs/index.md | 2 +- src/Testcontainers.Papercut/Testcontainers.Papercut.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e04826178..70c21f5e4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ $(Version) $(Version) Testcontainers - Copyright (c) 2019 - 2023 Andre Hofmeister and other authors + Copyright (c) 2019 - 2024 Andre Hofmeister and other authors Andre Hofmeister and contributors Andre Hofmeister Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions. diff --git a/LICENSE b/LICENSE index 83918c3a7..4ebf42b2d 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 6c59acf34..b27fd6184 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/index.md b/docs/index.md index 593b526f6..135e8c274 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj index 4ef9bff5f..e16b4437c 100644 --- a/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj +++ b/src/Testcontainers.Papercut/Testcontainers.Papercut.csproj @@ -2,7 +2,7 @@ netstandard2.0;netstandard2.1 latest - Copyright (c) 2019 - 2023 Liam Wilson, Andre Hofmeister and other authors + Copyright (c) 2019 - 2024 Liam Wilson, Andre Hofmeister and other authors Liam Wilson, Andre Hofmeister and contributors A Testcontainers Papercut module for testing SMTP clients and sending emails.