From 0c3eabf2c0556fa7d6c5a85841b82ca7d3ac3f65 Mon Sep 17 00:00:00 2001 From: William Godbe Date: Mon, 17 Oct 2022 10:49:21 -0700 Subject: [PATCH] Merged PR 25951: [internal/release/7.0] Use Path.GetTempFileName() for 600 Unix file mode (#44477) # {PR title} Summary of the changes (Less than 80 chars) ## Description {Detail} Fixes #{bug number} (in this specific format) ## Customer Impact {Justification} ## Regression? - [ ] Yes - [ ] No [If yes, specify the version the behavior has regressed from] ## Risk - [ ] High - [ ] Medium - [ ] Low [Justify the selection above] ## Verification - [ ] Manual (required) - [ ] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [ ] N/A ---- ## When servicing release/2.1 - [ ] Make necessary changes in eng/PatchConfig.props This is the simplest "backport" of https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore/pullrequest/25405 since we can use Path.GetTempFileName() on windows without worrying about hitting a limit, and we can use the Unix file mode APIs for tests. The other backports shouldn't be too bad though. Do we have a recommendation on how to test the unix file mode on backports? P/Inoking stat(2)? Don't bother? Cherry picked from !25566 Co-authored-by: Matt Mitchell --- .../Repositories/FileSystemXmlRepository.cs | 9 ++++++ .../FileSystemXmlRepositoryTests.cs | 19 +++++++++++++ .../src/FileBufferingReadStream.cs | 9 ++++++ .../src/FileBufferingWriteStream.cs | 9 ++++++ .../test/FileBufferingReadStreamTests.cs | 28 +++++++++++++++++++ .../test/FileBufferingWriteStreamTests.cs | 18 ++++++++++++ .../CertificateManager.cs | 17 +++++++++++ .../test/CertificateManagerTests.cs | 26 +++++++++++++++++ .../src/Commands/CreateCommand.cs | 9 +++--- .../dotnet-user-jwts/src/Helpers/JwtStore.cs | 16 ++++++++++- src/Tools/dotnet-user-jwts/src/Program.cs | 5 +++- .../dotnet-user-jwts/test/UserJwtsTests.cs | 15 ++++++++++ .../src/Internal/SecretsStore.cs | 11 ++++++++ src/Tools/dotnet-user-secrets/src/Program.cs | 7 +++++ .../test/SecretManagerTests.cs | 13 +++++++++ 15 files changed, 205 insertions(+), 6 deletions(-) diff --git a/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs b/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs index 472f45694e3d..7520bf1562b0 100644 --- a/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs +++ b/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Xml.Linq; using Microsoft.AspNetCore.DataProtection.Internal; using Microsoft.Extensions.Logging; @@ -131,9 +132,17 @@ private void StoreElementCore(XElement element, string filename) // crashes mid-write, we won't end up with a corrupt .xml file. Directory.Create(); // won't throw if the directory already exists + var tempFilename = Path.Combine(Directory.FullName, Guid.NewGuid().ToString() + ".tmp"); var finalFilename = Path.Combine(Directory.FullName, filename + ".xml"); + // Create a temp file with the correct Unix file mode before moving it to the expected finalFilename. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempTempFilename = Path.GetTempFileName(); + File.Move(tempTempFilename, tempFilename); + } + try { using (var tempFileStream = File.OpenWrite(tempFilename)) diff --git a/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs b/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs index de3a4ddaf74d..18ce2991d6ec 100644 --- a/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs +++ b/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs @@ -155,6 +155,25 @@ public void Logs_DockerEphemeralFolders() }); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void StoreElement_CreatesFileWithUserOnlyUnixFileMode() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var element = XElement.Parse(""); + var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance); + + // Act + repository.StoreElement(element, "friendly-name"); + + // Assert + var fileInfo = Assert.Single(dirInfo.GetFiles()); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, fileInfo.UnixFileMode); + }); + } + /// /// Runs a test and cleans up the temp directory afterward. /// diff --git a/src/Http/WebUtilities/src/FileBufferingReadStream.cs b/src/Http/WebUtilities/src/FileBufferingReadStream.cs index 11828e046f3a..898b6299a754 100644 --- a/src/Http/WebUtilities/src/FileBufferingReadStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingReadStream.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.WebUtilities; @@ -254,6 +255,14 @@ private Stream CreateTempFile() } _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); + + // Create a temp file with the correct Unix file mode before moving it to the assigned _tempFileName in the _tempFileDirectory. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempTempFileName = Path.GetTempFileName(); + File.Move(tempTempFileName, _tempFileName); + } + return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); } diff --git a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs index 7d3fce586927..1df345686dc2 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.WebUtilities; @@ -270,6 +271,14 @@ private void EnsureFileStream() { var tempFileDirectory = _tempFileDirectoryAccessor(); var tempFileName = Path.Combine(tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid() + ".tmp"); + + // Create a temp file with the correct Unix file mode before moving it to the assigned tempFileName in the _tempFileDirectory. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempTempFileName = Path.GetTempFileName(); + File.Move(tempTempFileName, tempFileName); + } + FileStream = new FileStream( tempFileName, FileMode.Create, diff --git a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs index c4bffe076f14..260b29feb9c0 100644 --- a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using Microsoft.AspNetCore.Testing; using Moq; namespace Microsoft.AspNetCore.WebUtilities; @@ -599,6 +600,33 @@ public async Task PartialReadAsyncThenSeekReplaysBuffer() Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray()); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void Read_BufferingContentToDisk_CreatesFileWithUserOnlyUnixFileMode() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) + { + var bytes = new byte[1024 * 2]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read1); + + tempFileName = stream.TempFileName!; + Assert.True(File.Exists(tempFileName)); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(tempFileName)); + } + + Assert.False(File.Exists(tempFileName)); + } + private static string GetCurrentDirectory() { return AppContext.BaseDirectory; diff --git a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs index 9fe101f07a05..5cbfee0090fd 100644 --- a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Text; +using Microsoft.AspNetCore.Testing; namespace Microsoft.AspNetCore.WebUtilities; @@ -365,6 +366,23 @@ public async Task DrainBufferAsync_WithContentInDisk_CopiesContentFromMemoryStre Assert.Equal(0, bufferingStream.Length); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void Write_BufferingContentToDisk_CreatesFileWithUserOnlyUnixFileMode() + { + // Arrange + var input = new byte[] { 1, 2, 3, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, 2); + + // Act + bufferingStream.Write(input, 2, 1); + + // Assert + Assert.NotNull(bufferingStream.FileStream); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(bufferingStream.FileStream.SafeFileHandle)); + } + public void Dispose() { try diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index aad1462eb1c1..97616be6d14a 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -548,6 +549,14 @@ internal static void ExportCertificate(X509Certificate2 certificate, string path try { Log.WriteCertificateToDisk(path); + + // Create a temp file with the correct Unix file mode before moving it to the expected path. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempFilename = Path.GetTempFileName(); + File.Move(tempFilename, path, overwrite: true); + } + File.WriteAllBytes(path, bytes); } catch (Exception ex) when (Log.IsEnabled()) @@ -568,6 +577,14 @@ internal static void ExportCertificate(X509Certificate2 certificate, string path { var keyPath = Path.ChangeExtension(path, ".key"); Log.WritePemKeyToDisk(keyPath); + + // Create a temp file with the correct Unix file mode before moving it to the expected path. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempFilename = Path.GetTempFileName(); + File.Move(tempFilename, keyPath, overwrite: true); + } + File.WriteAllBytes(keyPath, pemEnvelope); } catch (Exception ex) when (Log.IsEnabled()) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 0dacfb93fffb..568bd4d4753c 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -418,6 +418,32 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion() e.Oid.Value == CertificateManager.AspNetHttpsOid && e.RawData[0] == 1); } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "https://github.com/dotnet/aspnetcore/issues/6720")] + public void EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode() + { + _fixture.CleanupCertificates(); + + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode) + ".pem"; + const string KeyName = nameof(EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode) + ".key"; + + var certificatePassword = Guid.NewGuid().ToString(); + var now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + + var result = _manager + .EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false); + + Assert.Equal(EnsureCertificateResult.Succeeded, result); + + Assert.True(File.Exists(CertificateName)); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(CertificateName)); + + Assert.True(File.Exists(KeyName)); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(KeyName)); + } } public class CertFixture : IDisposable diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index efe415057f3a..ff8a1c4c8f17 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -20,7 +20,7 @@ internal sealed class CreateCommand @"s\s" }; - public static void Register(ProjectCommandLineApplication app) + public static void Register(ProjectCommandLineApplication app, Program program) { app.Command("create", cmd => { @@ -94,7 +94,7 @@ public static void Register(ProjectCommandLineApplication app) return 1; } - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, outputOption.Value()); + return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, outputOption.Value(), program); }); }); } @@ -227,7 +227,8 @@ private static int Execute( string projectPath, JwtCreatorOptions options, string optionsString, - string outputFormat) + string outputFormat, + Program program) { if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId)) { @@ -238,7 +239,7 @@ private static int Execute( var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial); var jwtToken = jwtIssuer.Create(options); - var jwtStore = new JwtStore(userSecretsId); + var jwtStore = new JwtStore(userSecretsId, program); var jwt = Jwt.Create(options.Scheme, jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims); if (options.Claims is { } customClaims) { diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs index 8bffc9d9c2ce..d9d2d68de794 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; using System.Text.Json; using Microsoft.Extensions.Configuration.UserSecrets; @@ -12,11 +13,17 @@ public class JwtStore private readonly string _userSecretsId; private readonly string _filePath; - public JwtStore(string userSecretsId) + public JwtStore(string userSecretsId, Program program = null) { _userSecretsId = userSecretsId; _filePath = Path.Combine(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(userSecretsId)), FileName); Load(); + + // For testing. + if (program is not null) + { + program.UserJwtsFilePath = _filePath; + } } public IDictionary Jwts { get; private set; } = new Dictionary(); @@ -37,6 +44,13 @@ public void Save() { if (Jwts is not null) { + // Create a temp file with the correct Unix file mode before moving it to the expected _filePath. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempFilename = Path.GetTempFileName(); + File.Move(tempFilename, _filePath, overwrite: true); + } + using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write); JsonSerializer.Serialize(fileStream, Jwts); } diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index 96ce31449cef..74ead8876d97 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -17,6 +17,9 @@ public Program(IConsole console) _reporter = new ConsoleReporter(console); } + // For testing. + internal string UserJwtsFilePath { get; set; } + public static void Main(string[] args) { new Program(PhysicalConsole.Singleton).Run(args); @@ -34,7 +37,7 @@ public void Run(string[] args) // dotnet user-jwts list ListCommand.Register(userJwts); // dotnet user-jwts create - CreateCommand.Register(userJwts); + CreateCommand.Register(userJwts, this); // dotnet user-jwts print ecd045 PrintCommand.Register(userJwts); // dotnet user-jwts remove ecd045 diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index 59118db081e1..f81e1387bc3f 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -580,4 +580,19 @@ public void List_CanHandleNoProjectOptionProvided_WithNoProjects() Assert.Contains("No project found at `-p|--project` path or current directory.", _console.GetOutput()); } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void Create_CreatesFileWithUserOnlyUnixFileMode() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + Assert.NotNull(app.UserJwtsFilePath); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(app.UserJwtsFilePath)); + } } diff --git a/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs b/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs index 9135afd0edd6..44313122bd01 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs +++ b/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.UserSecrets; @@ -46,6 +47,9 @@ public string this[string key] public int Count => _secrets.Count; + // For testing. + internal string SecretsFilePath => _secretsFilePath; + public bool ContainsKey(string key) => _secrets.ContainsKey(key); public IEnumerable> AsEnumerable() => _secrets; @@ -75,6 +79,13 @@ public virtual void Save() } } + // Create a temp file with the correct Unix file mode before moving it to the expected _filePath. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var tempFilename = Path.GetTempFileName(); + File.Move(tempFilename, _secretsFilePath, overwrite: true); + } + File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8); } diff --git a/src/Tools/dotnet-user-secrets/src/Program.cs b/src/Tools/dotnet-user-secrets/src/Program.cs index d6102f51631d..f01b7e434678 100644 --- a/src/Tools/dotnet-user-secrets/src/Program.cs +++ b/src/Tools/dotnet-user-secrets/src/Program.cs @@ -27,6 +27,9 @@ public Program(IConsole console, string workingDirectory) _workingDirectory = workingDirectory; } + // For testing. + internal string SecretsFilePath { get; private set; } + public bool TryRun(string[] args, out int returnCode) { try @@ -85,6 +88,10 @@ internal int RunInternal(params string[] args) var store = new SecretsStore(userSecretsId, reporter); var context = new Internal.CommandContext(store, reporter, _console); options.Command.Execute(context); + + // For testing. + SecretsFilePath = store.SecretsFilePath; + return 0; } diff --git a/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs b/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs index 53f6e9fddb6b..a3b5a733e101 100644 --- a/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs +++ b/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs @@ -338,4 +338,17 @@ public void Init_When_Project_Has_No_Secrets_Id() Assert.DoesNotContain(Resources.FormatError_ProjectMissingId(project), _console.GetOutput()); Assert.DoesNotContain("--help", _console.GetOutput()); } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void SetSecrets_CreatesFileWithUserOnlyUnixFileMode() + { + var projectPath = _fixture.GetTempSecretProject(); + var secretManager = new Program(_console, projectPath); + + secretManager.RunInternal("set", "key1", Guid.NewGuid().ToString(), "--verbose"); + + Assert.NotNull(secretManager.SecretsFilePath); + Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(secretManager.SecretsFilePath)); + } }