diff --git a/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs b/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs index 472f45694e3db87a925c825832234d65e19f7230..7520bf1562b0fa757d47780ec85c2f602970e9b8 100644 --- a/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs +++ b/src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs @@ -5,6 +5,7 @@ using System; 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 @@ public class FileSystemXmlRepository : IXmlRepository // 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 de3a4ddaf74db9e9a8f46197b190c5e95d78cc29..18ce2991d6eca3e9eac96fd7e40ffc25588f0a1a 100644 --- a/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs +++ b/src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs @@ -155,6 +155,25 @@ public class FileSystemXmlRepositoryTests }); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")] + public void StoreElement_CreatesFileWithUserOnlyUnixFileMode() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var element = XElement.Parse("<element1 />"); + 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); + }); + } + /// <summary> /// Runs a test and cleans up the temp directory afterward. /// </summary> diff --git a/src/Http/WebUtilities/src/FileBufferingReadStream.cs b/src/Http/WebUtilities/src/FileBufferingReadStream.cs index 11828e046f3a201e329c4900808d6cd5ff48bc93..898b6299a7545ac24f1c777940f4467befd2104f 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 @@ public class FileBufferingReadStream : Stream } _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 d73e7a2c392e703ebaa6d1cd926dc65df4041a7c..1d760cb269e9f0b0aebe34b008a0b2ecf8552757 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.WebUtilities; @@ -271,6 +272,14 @@ public sealed class FileBufferingWriteStream : Stream { 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 c4bffe076f14e9bfc445a3edac2dd706fdb7d6e9..260b29feb9c05cfa0ac4b9d4ee40273ae8b96cfc 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 class FileBufferingReadStreamTests 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 9fe101f07a0593da80853e57b0a7d0bd7eb4e91c..5cbfee0090fd075c77be5bad57f6099b6277ad76 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 class FileBufferingWriteStreamTests : IDisposable 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 0bd57f57de2248a925c529b0b3a37f8b68f11d64..c34d88f8c00c794f2b762ea87a9b446468467889 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics; 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 abstract class CertificateManager 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 abstract class CertificateManager { 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 0dacfb93fffb4f876f32f7555058788ddf2b450a..568bd4d4753ce4c85dc8828ef8b2959def0f7d25 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -418,6 +418,32 @@ public class CertificateManagerTests : IClassFixture<CertFixture> 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 efe415057f3a1956bba45a2f56599ce643a44866..ff8a1c4c8f17af5ec19aa3acbc0bda65f0efde15 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 @@ internal sealed class CreateCommand 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 @@ internal sealed class CreateCommand 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 @@ internal sealed class CreateCommand 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 8bffc9d9c2ce089877d7c63b323925077ade2bb9..d9d2d68de794f62ccbb60f582e6543d6123fc231 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<string, Jwt> Jwts { get; private set; } = new Dictionary<string, Jwt>(); @@ -37,6 +44,13 @@ public class JwtStore { 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 96ce31449cef451243a8ef223311363185b12c5d..74ead8876d970bc974fcaa66476f896aa67ae81c 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -17,6 +17,9 @@ public class Program _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 class Program // 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 59118db081e15f951248b0937054f16a72dec40a..f81e1387bc3f86ce8d61749824f8fbad5b7dc1a1 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -580,4 +580,19 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> 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 9135afd0edd6289261534125369fb19b91e1cef8..44313122bd0109e48aef6a4f427c64da274cac45 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; 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 class SecretsStore public int Count => _secrets.Count; + // For testing. + internal string SecretsFilePath => _secretsFilePath; + public bool ContainsKey(string key) => _secrets.ContainsKey(key); public IEnumerable<KeyValuePair<string, string>> AsEnumerable() => _secrets; @@ -75,6 +79,13 @@ public class SecretsStore } } + // 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 d6102f51631d3246707e373fda25af1df36f5c70..f01b7e434678a7e3a4b74734328e42b85d792155 100644 --- a/src/Tools/dotnet-user-secrets/src/Program.cs +++ b/src/Tools/dotnet-user-secrets/src/Program.cs @@ -27,6 +27,9 @@ public class Program _workingDirectory = workingDirectory; } + // For testing. + internal string SecretsFilePath { get; private set; } + public bool TryRun(string[] args, out int returnCode) { try @@ -85,6 +88,10 @@ public class Program 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 d16b9173f4962a6558d3c484d3571252f27d4aed..7edb305491412281edb72ab40b3176c0c703f0ee 100644 --- a/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs +++ b/src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs @@ -338,4 +338,17 @@ public class SecretManagerTests : IClassFixture<UserSecretsTestFixture> 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)); + } }