Docker: add warning when FileSystemXmlRepository saves a key to non-volume mounted folder
This commit is contained in:
parent
ddd041b0f1
commit
abf05e2856
|
|
@ -1,13 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\build\dependencies.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
|
||||
<OutputType>exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:1394/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"NonDISample": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,64 +15,56 @@ namespace Microsoft.AspNetCore.Cryptography.Internal
|
|||
/// </summary>
|
||||
internal static string BCryptAlgorithmHandle_ProviderNotFound
|
||||
{
|
||||
get { return GetString("BCryptAlgorithmHandle_ProviderNotFound"); }
|
||||
get => GetString("BCryptAlgorithmHandle_ProviderNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A provider could not be found for algorithm '{0}'.
|
||||
/// </summary>
|
||||
internal static string FormatBCryptAlgorithmHandle_ProviderNotFound(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("BCryptAlgorithmHandle_ProviderNotFound"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("BCryptAlgorithmHandle_ProviderNotFound"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).
|
||||
/// </summary>
|
||||
internal static string BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength
|
||||
{
|
||||
get { return GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"); }
|
||||
get => GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).
|
||||
/// </summary>
|
||||
internal static string FormatBCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"), p0, p1, p2, p3);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"), p0, p1, p2, p3);
|
||||
|
||||
/// <summary>
|
||||
/// This operation requires Windows 7 / Windows Server 2008 R2 or later.
|
||||
/// </summary>
|
||||
internal static string Platform_Windows7Required
|
||||
{
|
||||
get { return GetString("Platform_Windows7Required"); }
|
||||
get => GetString("Platform_Windows7Required");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This operation requires Windows 7 / Windows Server 2008 R2 or later.
|
||||
/// </summary>
|
||||
internal static string FormatPlatform_Windows7Required()
|
||||
{
|
||||
return GetString("Platform_Windows7Required");
|
||||
}
|
||||
=> GetString("Platform_Windows7Required");
|
||||
|
||||
/// <summary>
|
||||
/// This operation requires Windows 8 / Windows Server 2012 or later.
|
||||
/// </summary>
|
||||
internal static string Platform_Windows8Required
|
||||
{
|
||||
get { return GetString("Platform_Windows8Required"); }
|
||||
get => GetString("Platform_Windows8Required");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This operation requires Windows 8 / Windows Server 2012 or later.
|
||||
/// </summary>
|
||||
internal static string FormatPlatform_Windows8Required()
|
||||
{
|
||||
return GetString("Platform_Windows8Required");
|
||||
}
|
||||
=> GetString("Platform_Windows8Required");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,64 +15,56 @@ namespace Microsoft.AspNetCore.DataProtection.Abstractions
|
|||
/// </summary>
|
||||
internal static string CryptCommon_PayloadInvalid
|
||||
{
|
||||
get { return GetString("CryptCommon_PayloadInvalid"); }
|
||||
get => GetString("CryptCommon_PayloadInvalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The payload was invalid.
|
||||
/// </summary>
|
||||
internal static string FormatCryptCommon_PayloadInvalid()
|
||||
{
|
||||
return GetString("CryptCommon_PayloadInvalid");
|
||||
}
|
||||
=> GetString("CryptCommon_PayloadInvalid");
|
||||
|
||||
/// <summary>
|
||||
/// The purposes collection cannot be null or empty and cannot contain null elements.
|
||||
/// </summary>
|
||||
internal static string DataProtectionExtensions_NullPurposesCollection
|
||||
{
|
||||
get { return GetString("DataProtectionExtensions_NullPurposesCollection"); }
|
||||
get => GetString("DataProtectionExtensions_NullPurposesCollection");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The purposes collection cannot be null or empty and cannot contain null elements.
|
||||
/// </summary>
|
||||
internal static string FormatDataProtectionExtensions_NullPurposesCollection()
|
||||
{
|
||||
return GetString("DataProtectionExtensions_NullPurposesCollection");
|
||||
}
|
||||
=> GetString("DataProtectionExtensions_NullPurposesCollection");
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during a cryptographic operation.
|
||||
/// </summary>
|
||||
internal static string CryptCommon_GenericError
|
||||
{
|
||||
get { return GetString("CryptCommon_GenericError"); }
|
||||
get => GetString("CryptCommon_GenericError");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during a cryptographic operation.
|
||||
/// </summary>
|
||||
internal static string FormatCryptCommon_GenericError()
|
||||
{
|
||||
return GetString("CryptCommon_GenericError");
|
||||
}
|
||||
=> GetString("CryptCommon_GenericError");
|
||||
|
||||
/// <summary>
|
||||
/// No service for type '{0}' has been registered.
|
||||
/// </summary>
|
||||
internal static string DataProtectionExtensions_NoService
|
||||
{
|
||||
get { return GetString("DataProtectionExtensions_NoService"); }
|
||||
get => GetString("DataProtectionExtensions_NoService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No service for type '{0}' has been registered.
|
||||
/// </summary>
|
||||
internal static string FormatDataProtectionExtensions_NoService(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("DataProtectionExtensions_NoService"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DataProtectionExtensions_NoService"), p0);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,48 +15,42 @@ namespace Microsoft.AspNetCore.DataProtection.Extensions
|
|||
/// </summary>
|
||||
internal static string CryptCommon_GenericError
|
||||
{
|
||||
get { return GetString("CryptCommon_GenericError"); }
|
||||
get => GetString("CryptCommon_GenericError");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during a cryptographic operation.
|
||||
/// </summary>
|
||||
internal static string FormatCryptCommon_GenericError()
|
||||
{
|
||||
return GetString("CryptCommon_GenericError");
|
||||
}
|
||||
=> GetString("CryptCommon_GenericError");
|
||||
|
||||
/// <summary>
|
||||
/// The payload expired at {0}.
|
||||
/// </summary>
|
||||
internal static string TimeLimitedDataProtector_PayloadExpired
|
||||
{
|
||||
get { return GetString("TimeLimitedDataProtector_PayloadExpired"); }
|
||||
get => GetString("TimeLimitedDataProtector_PayloadExpired");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The payload expired at {0}.
|
||||
/// </summary>
|
||||
internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The payload is invalid.
|
||||
/// </summary>
|
||||
internal static string TimeLimitedDataProtector_PayloadInvalid
|
||||
{
|
||||
get { return GetString("TimeLimitedDataProtector_PayloadInvalid"); }
|
||||
get => GetString("TimeLimitedDataProtector_PayloadInvalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The payload is invalid.
|
||||
/// </summary>
|
||||
internal static string FormatTimeLimitedDataProtector_PayloadInvalid()
|
||||
{
|
||||
return GetString("TimeLimitedDataProtector_PayloadInvalid");
|
||||
}
|
||||
=> GetString("TimeLimitedDataProtector_PayloadInvalid");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,32 +15,28 @@ namespace Microsoft.AspNetCore.DataProtection.SystemWeb
|
|||
/// </summary>
|
||||
internal static string DataProtector_ProtectFailed
|
||||
{
|
||||
get { return GetString("DataProtector_ProtectFailed"); }
|
||||
get => GetString("DataProtector_ProtectFailed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
|
||||
/// </summary>
|
||||
internal static string FormatDataProtector_ProtectFailed()
|
||||
{
|
||||
return GetString("DataProtector_ProtectFailed");
|
||||
}
|
||||
=> GetString("DataProtector_ProtectFailed");
|
||||
|
||||
/// <summary>
|
||||
/// The CreateDataProtectionProvider method returned null.
|
||||
/// </summary>
|
||||
internal static string Startup_CreateProviderReturnedNull
|
||||
{
|
||||
get { return GetString("Startup_CreateProviderReturnedNull"); }
|
||||
get => GetString("Startup_CreateProviderReturnedNull");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CreateDataProtectionProvider method returned null.
|
||||
/// </summary>
|
||||
internal static string FormatStartup_CreateProviderReturnedNull()
|
||||
{
|
||||
return GetString("Startup_CreateProviderReturnedNull");
|
||||
}
|
||||
=> GetString("Startup_CreateProviderReturnedNull");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Internal
|
||||
{
|
||||
internal static class DockerUtils
|
||||
{
|
||||
private static Lazy<bool> _isDocker = new Lazy<bool>(IsProcessRunningInDocker);
|
||||
|
||||
public static bool IsDocker => _isDocker.Value;
|
||||
|
||||
public static bool IsVolumeMountedFolder(DirectoryInfo directory)
|
||||
{
|
||||
if (!IsDocker)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// we currently don't have a good way to detect mounted file systems within Windows ctonainers
|
||||
return false;
|
||||
}
|
||||
|
||||
const string mountsFile = "/proc/self/mounts";
|
||||
if (!File.Exists(mountsFile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lines = File.ReadAllLines(mountsFile);
|
||||
return IsDirectoryMounted(directory, lines);
|
||||
}
|
||||
|
||||
// internal for testing. Don't use directly
|
||||
internal static bool IsDirectoryMounted(DirectoryInfo directory, IEnumerable<string> fstab)
|
||||
{
|
||||
// Expected file format: http://man7.org/linux/man-pages/man5/fstab.5.html
|
||||
foreach (var line in fstab)
|
||||
{
|
||||
if (line == null || line.Length == 0 || line[0] == '#')
|
||||
{
|
||||
// skip empty and commented-out lines
|
||||
continue;
|
||||
}
|
||||
|
||||
var fields = line.Split(new[] { '\t', ' ' });
|
||||
|
||||
if (fields.Length < 2 // line had too few fields
|
||||
|| fields[1].Length <= 1 // fs_file empty or is the root directory '/'
|
||||
|| fields[1][0] != '/') // fs_file was not a file path
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if directory is a subdirectory of this location
|
||||
var fs_file = new DirectoryInfo(fields[1].TrimEnd(Path.DirectorySeparatorChar)).FullName;
|
||||
var dir = directory;
|
||||
while (dir != null)
|
||||
{
|
||||
// filesystems on Linux are case sensitive
|
||||
if (fs_file.Equals(dir.FullName.TrimEnd(Path.DirectorySeparatorChar), StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsProcessRunningInDocker()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// we currently don't have a good way to detect if running in a Windows container
|
||||
return false;
|
||||
}
|
||||
|
||||
const string procFile = "/proc/1/cgroup";
|
||||
if (!File.Exists(procFile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lines = File.ReadAllLines(procFile);
|
||||
// typically the last line in the file is "1:name=openrc:/docker"
|
||||
return lines.Reverse().Any(l => l.EndsWith("name=openrc:/docker", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.Extensions.Logging
|
||||
|
|
@ -143,6 +144,8 @@ namespace Microsoft.Extensions.Logging
|
|||
|
||||
private static Action<ILogger, string, Exception> _usingAzureAsKeyRepository;
|
||||
|
||||
private static Action<ILogger, string, Exception> _usingEphemeralFileSystemLocationInContainer;
|
||||
|
||||
static LoggingExtensions()
|
||||
{
|
||||
_usingFallbackKeyWithExpirationAsDefaultKey = LoggerMessage.Define<Guid, DateTimeOffset>(
|
||||
|
|
@ -377,19 +380,29 @@ namespace Microsoft.Extensions.Logging
|
|||
eventId: 58,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "Creating key {KeyId:B} with creation date {CreationDate:u}, activation date {ActivationDate:u}, and expiration date {ExpirationDate:u}.");
|
||||
_usingEphemeralKeyRepository = LoggerMessage.Define(eventId: 59,
|
||||
_usingEphemeralKeyRepository = LoggerMessage.Define(
|
||||
eventId: 59,
|
||||
logLevel: LogLevel.Warning,
|
||||
formatString: "Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.");
|
||||
_usingRegistryAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(eventId: 0,
|
||||
_usingEphemeralFileSystemLocationInContainer = LoggerMessage.Define<string>(
|
||||
eventId: 60,
|
||||
logLevel: LogLevel.Warning,
|
||||
formatString: Resources.FileSystem_EphemeralKeysLocationInContainer);
|
||||
|
||||
_usingRegistryAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(
|
||||
eventId: 0,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "User profile not available. Using '{Name}' as key repository and Windows DPAPI to encrypt keys at rest.");
|
||||
_usingProfileAsKeyRepository = LoggerMessage.Define<string>(eventId: 0,
|
||||
_usingProfileAsKeyRepository = LoggerMessage.Define<string>(
|
||||
eventId: 0,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "User profile is available. Using '{FullName}' as key repository; keys will not be encrypted at rest.");
|
||||
_usingProfileAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(eventId: 0,
|
||||
_usingProfileAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(
|
||||
eventId: 0,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "User profile is available. Using '{FullName}' as key repository and Windows DPAPI to encrypt keys at rest.");
|
||||
_usingAzureAsKeyRepository = LoggerMessage.Define<string>(eventId: 0,
|
||||
_usingAzureAsKeyRepository = LoggerMessage.Define<string>(
|
||||
eventId: 0,
|
||||
logLevel: LogLevel.Information,
|
||||
formatString: "Azure Web Sites environment detected. Using '{FullName}' as key repository; keys will not be encrypted at rest.");
|
||||
_keyRingWasLoadedOnStartup = LoggerMessage.Define<Guid>(
|
||||
|
|
@ -782,5 +795,10 @@ namespace Microsoft.Extensions.Logging
|
|||
{
|
||||
_keyRingFailedToLoadOnStartup(logger, innerException);
|
||||
}
|
||||
|
||||
public static void UsingEphemeralFileSystemLocationInContainer(this ILogger logger, string path)
|
||||
{
|
||||
_usingEphemeralFileSystemLocationInContainer(logger, path, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,401 +15,365 @@ namespace Microsoft.AspNetCore.DataProtection
|
|||
/// </summary>
|
||||
internal static string CryptCommon_GenericError
|
||||
{
|
||||
get { return GetString("CryptCommon_GenericError"); }
|
||||
get => GetString("CryptCommon_GenericError");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during a cryptographic operation.
|
||||
/// </summary>
|
||||
internal static string FormatCryptCommon_GenericError()
|
||||
{
|
||||
return GetString("CryptCommon_GenericError");
|
||||
}
|
||||
=> GetString("CryptCommon_GenericError");
|
||||
|
||||
/// <summary>
|
||||
/// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length.
|
||||
/// </summary>
|
||||
internal static string Common_BufferIncorrectlySized
|
||||
{
|
||||
get { return GetString("Common_BufferIncorrectlySized"); }
|
||||
get => GetString("Common_BufferIncorrectlySized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_BufferIncorrectlySized(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("Common_BufferIncorrectlySized"), p0, p1);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("Common_BufferIncorrectlySized"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The payload was invalid.
|
||||
/// </summary>
|
||||
internal static string CryptCommon_PayloadInvalid
|
||||
{
|
||||
get { return GetString("CryptCommon_PayloadInvalid"); }
|
||||
get => GetString("CryptCommon_PayloadInvalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The payload was invalid.
|
||||
/// </summary>
|
||||
internal static string FormatCryptCommon_PayloadInvalid()
|
||||
{
|
||||
return GetString("CryptCommon_PayloadInvalid");
|
||||
}
|
||||
=> GetString("CryptCommon_PayloadInvalid");
|
||||
|
||||
/// <summary>
|
||||
/// Property {0} cannot be null or empty.
|
||||
/// </summary>
|
||||
internal static string Common_PropertyCannotBeNullOrEmpty
|
||||
{
|
||||
get { return GetString("Common_PropertyCannotBeNullOrEmpty"); }
|
||||
get => GetString("Common_PropertyCannotBeNullOrEmpty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property {0} cannot be null or empty.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_PropertyCannotBeNullOrEmpty(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyCannotBeNullOrEmpty"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyCannotBeNullOrEmpty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload could not be decrypted. Refer to the inner exception for more information.
|
||||
/// </summary>
|
||||
internal static string Common_DecryptionFailed
|
||||
{
|
||||
get { return GetString("Common_DecryptionFailed"); }
|
||||
get => GetString("Common_DecryptionFailed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload could not be decrypted. Refer to the inner exception for more information.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_DecryptionFailed()
|
||||
{
|
||||
return GetString("Common_DecryptionFailed");
|
||||
}
|
||||
=> GetString("Common_DecryptionFailed");
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.
|
||||
/// </summary>
|
||||
internal static string Common_EncryptionFailed
|
||||
{
|
||||
get { return GetString("Common_EncryptionFailed"); }
|
||||
get => GetString("Common_EncryptionFailed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_EncryptionFailed()
|
||||
{
|
||||
return GetString("Common_EncryptionFailed");
|
||||
}
|
||||
=> GetString("Common_EncryptionFailed");
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} was not found in the key ring.
|
||||
/// </summary>
|
||||
internal static string Common_KeyNotFound
|
||||
{
|
||||
get { return GetString("Common_KeyNotFound"); }
|
||||
get => GetString("Common_KeyNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} was not found in the key ring.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_KeyNotFound()
|
||||
{
|
||||
return GetString("Common_KeyNotFound");
|
||||
}
|
||||
=> GetString("Common_KeyNotFound");
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} has been revoked.
|
||||
/// </summary>
|
||||
internal static string Common_KeyRevoked
|
||||
{
|
||||
get { return GetString("Common_KeyRevoked"); }
|
||||
get => GetString("Common_KeyRevoked");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} has been revoked.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_KeyRevoked()
|
||||
{
|
||||
return GetString("Common_KeyRevoked");
|
||||
}
|
||||
=> GetString("Common_KeyRevoked");
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload cannot be decrypted because it was not protected with this protection provider.
|
||||
/// </summary>
|
||||
internal static string ProtectionProvider_BadMagicHeader
|
||||
{
|
||||
get { return GetString("ProtectionProvider_BadMagicHeader"); }
|
||||
get => GetString("ProtectionProvider_BadMagicHeader");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload cannot be decrypted because it was not protected with this protection provider.
|
||||
/// </summary>
|
||||
internal static string FormatProtectionProvider_BadMagicHeader()
|
||||
{
|
||||
return GetString("ProtectionProvider_BadMagicHeader");
|
||||
}
|
||||
=> GetString("ProtectionProvider_BadMagicHeader");
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
|
||||
/// </summary>
|
||||
internal static string ProtectionProvider_BadVersion
|
||||
{
|
||||
get { return GetString("ProtectionProvider_BadVersion"); }
|
||||
get => GetString("ProtectionProvider_BadVersion");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
|
||||
/// </summary>
|
||||
internal static string FormatProtectionProvider_BadVersion()
|
||||
{
|
||||
return GetString("ProtectionProvider_BadVersion");
|
||||
}
|
||||
=> GetString("ProtectionProvider_BadVersion");
|
||||
|
||||
/// <summary>
|
||||
/// Value must be non-negative.
|
||||
/// </summary>
|
||||
internal static string Common_ValueMustBeNonNegative
|
||||
{
|
||||
get { return GetString("Common_ValueMustBeNonNegative"); }
|
||||
get => GetString("Common_ValueMustBeNonNegative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value must be non-negative.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_ValueMustBeNonNegative()
|
||||
{
|
||||
return GetString("Common_ValueMustBeNonNegative");
|
||||
}
|
||||
=> GetString("Common_ValueMustBeNonNegative");
|
||||
|
||||
/// <summary>
|
||||
/// The type '{1}' is not assignable to '{0}'.
|
||||
/// </summary>
|
||||
internal static string TypeExtensions_BadCast
|
||||
{
|
||||
get { return GetString("TypeExtensions_BadCast"); }
|
||||
get => GetString("TypeExtensions_BadCast");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{1}' is not assignable to '{0}'.
|
||||
/// </summary>
|
||||
internal static string FormatTypeExtensions_BadCast(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TypeExtensions_BadCast"), p0, p1);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TypeExtensions_BadCast"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The new key lifetime must be at least one week.
|
||||
/// </summary>
|
||||
internal static string KeyManagementOptions_MinNewKeyLifetimeViolated
|
||||
{
|
||||
get { return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated"); }
|
||||
get => GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The new key lifetime must be at least one week.
|
||||
/// </summary>
|
||||
internal static string FormatKeyManagementOptions_MinNewKeyLifetimeViolated()
|
||||
{
|
||||
return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
|
||||
}
|
||||
=> GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} already exists in the keyring.
|
||||
/// </summary>
|
||||
internal static string XmlKeyManager_DuplicateKey
|
||||
{
|
||||
get { return GetString("XmlKeyManager_DuplicateKey"); }
|
||||
get => GetString("XmlKeyManager_DuplicateKey");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key {0:B} already exists in the keyring.
|
||||
/// </summary>
|
||||
internal static string FormatXmlKeyManager_DuplicateKey()
|
||||
{
|
||||
return GetString("XmlKeyManager_DuplicateKey");
|
||||
}
|
||||
=> GetString("XmlKeyManager_DuplicateKey");
|
||||
|
||||
/// <summary>
|
||||
/// Argument cannot be null or empty.
|
||||
/// </summary>
|
||||
internal static string Common_ArgumentCannotBeNullOrEmpty
|
||||
{
|
||||
get { return GetString("Common_ArgumentCannotBeNullOrEmpty"); }
|
||||
get => GetString("Common_ArgumentCannotBeNullOrEmpty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Argument cannot be null or empty.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_ArgumentCannotBeNullOrEmpty()
|
||||
{
|
||||
return GetString("Common_ArgumentCannotBeNullOrEmpty");
|
||||
}
|
||||
=> GetString("Common_ArgumentCannotBeNullOrEmpty");
|
||||
|
||||
/// <summary>
|
||||
/// Property {0} must have a non-negative value.
|
||||
/// </summary>
|
||||
internal static string Common_PropertyMustBeNonNegative
|
||||
{
|
||||
get { return GetString("Common_PropertyMustBeNonNegative"); }
|
||||
get => GetString("Common_PropertyMustBeNonNegative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property {0} must have a non-negative value.
|
||||
/// </summary>
|
||||
internal static string FormatCommon_PropertyMustBeNonNegative(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyMustBeNonNegative"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyMustBeNonNegative"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// GCM algorithms require the Windows platform.
|
||||
/// </summary>
|
||||
internal static string Platform_WindowsRequiredForGcm
|
||||
{
|
||||
get { return GetString("Platform_WindowsRequiredForGcm"); }
|
||||
get => GetString("Platform_WindowsRequiredForGcm");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GCM algorithms require the Windows platform.
|
||||
/// </summary>
|
||||
internal static string FormatPlatform_WindowsRequiredForGcm()
|
||||
{
|
||||
return GetString("Platform_WindowsRequiredForGcm");
|
||||
}
|
||||
=> GetString("Platform_WindowsRequiredForGcm");
|
||||
|
||||
/// <summary>
|
||||
/// A certificate with the thumbprint '{0}' could not be found.
|
||||
/// </summary>
|
||||
internal static string CertificateXmlEncryptor_CertificateNotFound
|
||||
{
|
||||
get { return GetString("CertificateXmlEncryptor_CertificateNotFound"); }
|
||||
get => GetString("CertificateXmlEncryptor_CertificateNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A certificate with the thumbprint '{0}' could not be found.
|
||||
/// </summary>
|
||||
internal static string FormatCertificateXmlEncryptor_CertificateNotFound(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("CertificateXmlEncryptor_CertificateNotFound"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CertificateXmlEncryptor_CertificateNotFound"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
|
||||
/// </summary>
|
||||
internal static string EncryptedXmlDecryptor_DoesNotWorkOnCoreClr
|
||||
{
|
||||
get { return GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr"); }
|
||||
get => GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
|
||||
/// </summary>
|
||||
internal static string FormatEncryptedXmlDecryptor_DoesNotWorkOnCoreClr()
|
||||
{
|
||||
return GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
|
||||
}
|
||||
=> GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
|
||||
|
||||
/// <summary>
|
||||
/// The symmetric algorithm block size of {0} bits is invalid. The block size must be between 64 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string AlgorithmAssert_BadBlockSize
|
||||
{
|
||||
get { return GetString("AlgorithmAssert_BadBlockSize"); }
|
||||
get => GetString("AlgorithmAssert_BadBlockSize");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The symmetric algorithm block size of {0} bits is invalid. The block size must be between 64 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string FormatAlgorithmAssert_BadBlockSize(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadBlockSize"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadBlockSize"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The validation algorithm digest size of {0} bits is invalid. The digest size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string AlgorithmAssert_BadDigestSize
|
||||
{
|
||||
get { return GetString("AlgorithmAssert_BadDigestSize"); }
|
||||
get => GetString("AlgorithmAssert_BadDigestSize");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The validation algorithm digest size of {0} bits is invalid. The digest size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string FormatAlgorithmAssert_BadDigestSize(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadDigestSize"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadDigestSize"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string AlgorithmAssert_BadKeySize
|
||||
{
|
||||
get { return GetString("AlgorithmAssert_BadKeySize"); }
|
||||
get => GetString("AlgorithmAssert_BadKeySize");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
|
||||
/// </summary>
|
||||
internal static string FormatAlgorithmAssert_BadKeySize(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
|
||||
/// </summary>
|
||||
internal static string KeyRingProvider_NoDefaultKey_AutoGenerateDisabled
|
||||
{
|
||||
get { return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled"); }
|
||||
get => GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
|
||||
/// </summary>
|
||||
internal static string FormatKeyRingProvider_NoDefaultKey_AutoGenerateDisabled()
|
||||
{
|
||||
return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
|
||||
}
|
||||
=> GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
|
||||
|
||||
/// <summary>
|
||||
/// {0} must not be negative
|
||||
/// </summary>
|
||||
internal static string LifetimeMustNotBeNegative
|
||||
{
|
||||
get { return GetString("LifetimeMustNotBeNegative"); }
|
||||
get => GetString("LifetimeMustNotBeNegative");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// {0} must not be negative
|
||||
/// </summary>
|
||||
internal static string FormatLifetimeMustNotBeNegative(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("LifetimeMustNotBeNegative"), p0);
|
||||
}
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("LifetimeMustNotBeNegative"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.
|
||||
/// </summary>
|
||||
internal static string XmlKeyManager_IXmlRepositoryNotFound
|
||||
{
|
||||
get { return GetString("XmlKeyManager_IXmlRepositoryNotFound"); }
|
||||
get => GetString("XmlKeyManager_IXmlRepositoryNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.
|
||||
/// </summary>
|
||||
internal static string FormatXmlKeyManager_IXmlRepositoryNotFound(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("XmlKeyManager_IXmlRepositoryNotFound"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
|
||||
/// </summary>
|
||||
internal static string FileSystem_EphemeralKeysLocationInContainer
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("XmlKeyManager_IXmlRepositoryNotFound"), p0, p1);
|
||||
get => GetString("FileSystem_EphemeralKeysLocationInContainer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
|
||||
/// </summary>
|
||||
internal static string FormatFileSystem_EphemeralKeysLocationInContainer(object path)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("FileSystem_EphemeralKeysLocationInContainer", "path"), path);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.DataProtection.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Repositories
|
||||
|
|
@ -35,6 +36,22 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
|
|||
|
||||
Directory = directory;
|
||||
_logger = loggerFactory.CreateLogger<FileSystemXmlRepository>();
|
||||
|
||||
try
|
||||
{
|
||||
if (DockerUtils.IsDocker && !DockerUtils.IsVolumeMountedFolder(Directory))
|
||||
{
|
||||
// warn users that keys may be lost when running in docker without a volume mounted folder
|
||||
_logger.UsingEphemeralFileSystemLocationInContainer(Directory.FullName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Treat exceptions as non-fatal when attempting to detect docker.
|
||||
// These might occur if fstab is an unrecognized format, or if there are other unusual
|
||||
// file IO errors.
|
||||
_logger.LogTrace(ex, "Failure occurred while attempting to detect docker.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -192,4 +192,7 @@
|
|||
<data name="XmlKeyManager_IXmlRepositoryNotFound" xml:space="preserve">
|
||||
<value>The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.</value>
|
||||
</data>
|
||||
<data name="FileSystem_EphemeralKeysLocationInContainer" xml:space="preserve">
|
||||
<value>Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.DataProtection.Internal;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Test
|
||||
{
|
||||
public class DockerUtilsTests
|
||||
{
|
||||
// example of content from /proc/self/mounts
|
||||
private static readonly string[] fstab = new []
|
||||
{
|
||||
"none / aufs rw,relatime,si=f9bfcf896de3f6c2,dio,dirperm1 0 0",
|
||||
"# comments",
|
||||
"",
|
||||
"proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0",
|
||||
"tmpfs /dev tmpfs rw,nosuid,mode=755 0 0",
|
||||
"devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0",
|
||||
"/dev/vda2 /etc/resolv.conf ext4 rw,relatime,data=ordered 0 0",
|
||||
"/dev/vda2 /etc/hostname ext4 rw,relatime,data=ordered 0 0",
|
||||
"/dev/vda2 /etc/hosts ext4 rw,relatime,data=ordered 0 0",
|
||||
"shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0",
|
||||
// the mounted directory
|
||||
"osxfs /app fuse.osxfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other,max_read=1048576 0 0",
|
||||
};
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
[InlineData("/")]
|
||||
[InlineData("/home")]
|
||||
[InlineData("/home/")]
|
||||
[InlineData("/home/root")]
|
||||
[InlineData("./dir")]
|
||||
[InlineData("../dir")]
|
||||
public void DeterminesFolderIsNotMounted(string directory)
|
||||
{
|
||||
Assert.False(DockerUtils.IsDirectoryMounted(new DirectoryInfo(directory), fstab));
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
[InlineData("/app")]
|
||||
[InlineData("/app/")]
|
||||
[InlineData("/app/subdir")]
|
||||
[InlineData("/app/subdir/")]
|
||||
[InlineData("/app/subdir/two")]
|
||||
[InlineData("/app/subdir/two/")]
|
||||
public void DeterminesFolderIsMounted(string directory)
|
||||
{
|
||||
Assert.True(DockerUtils.IsDirectoryMounted(new DirectoryInfo(directory), fstab));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -139,6 +141,23 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
|
|||
});
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[DockerOnly]
|
||||
[Trait("Docker", "true")]
|
||||
public void Logs_DockerEphemeralFolders()
|
||||
{
|
||||
// Arrange
|
||||
var loggerFactory = new StringLoggerFactory(LogLevel.Warning);
|
||||
WithUniqueTempDirectory(dirInfo =>
|
||||
{
|
||||
// Act
|
||||
var repo = new FileSystemXmlRepository(dirInfo, loggerFactory);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(Resources.FormatFileSystem_EphemeralKeysLocationInContainer(dirInfo.FullName), loggerFactory.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a test and cleans up the temp directory afterward.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue