Skip to content

Commit 7b7659c

Browse files
committed
Merged PR 25951: [internal/release/7.0] Use Path.GetTempFileName() for 600 Unix file mode
# {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
1 parent 941eb27 commit 7b7659c

File tree

15 files changed

+205
-6
lines changed

15 files changed

+205
-6
lines changed

src/DataProtection/DataProtection/src/Repositories/FileSystemXmlRepository.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Runtime.InteropServices;
89
using System.Xml.Linq;
910
using Microsoft.AspNetCore.DataProtection.Internal;
1011
using Microsoft.Extensions.Logging;
@@ -131,9 +132,17 @@ private void StoreElementCore(XElement element, string filename)
131132
// crashes mid-write, we won't end up with a corrupt .xml file.
132133

133134
Directory.Create(); // won't throw if the directory already exists
135+
134136
var tempFilename = Path.Combine(Directory.FullName, Guid.NewGuid().ToString() + ".tmp");
135137
var finalFilename = Path.Combine(Directory.FullName, filename + ".xml");
136138

139+
// Create a temp file with the correct Unix file mode before moving it to the expected finalFilename.
140+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
141+
{
142+
var tempTempFilename = Path.GetTempFileName();
143+
File.Move(tempTempFilename, tempFilename);
144+
}
145+
137146
try
138147
{
139148
using (var tempFileStream = File.OpenWrite(tempFilename))

src/DataProtection/DataProtection/test/Repositories/FileSystemXmlRepositoryTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,25 @@ public void Logs_DockerEphemeralFolders()
155155
});
156156
}
157157

158+
[ConditionalFact]
159+
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")]
160+
public void StoreElement_CreatesFileWithUserOnlyUnixFileMode()
161+
{
162+
WithUniqueTempDirectory(dirInfo =>
163+
{
164+
// Arrange
165+
var element = XElement.Parse("<element1 />");
166+
var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
167+
168+
// Act
169+
repository.StoreElement(element, "friendly-name");
170+
171+
// Assert
172+
var fileInfo = Assert.Single(dirInfo.GetFiles());
173+
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, fileInfo.UnixFileMode);
174+
});
175+
}
176+
158177
/// <summary>
159178
/// Runs a test and cleans up the temp directory afterward.
160179
/// </summary>

src/Http/WebUtilities/src/FileBufferingReadStream.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
7+
using System.Runtime.InteropServices;
78
using Microsoft.AspNetCore.Internal;
89

910
namespace Microsoft.AspNetCore.WebUtilities;
@@ -254,6 +255,14 @@ private Stream CreateTempFile()
254255
}
255256

256257
_tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp");
258+
259+
// Create a temp file with the correct Unix file mode before moving it to the assigned _tempFileName in the _tempFileDirectory.
260+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
261+
{
262+
var tempTempFileName = Path.GetTempFileName();
263+
File.Move(tempTempFileName, _tempFileName);
264+
}
265+
257266
return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16,
258267
FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan);
259268
}

src/Http/WebUtilities/src/FileBufferingWriteStream.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
77
using System.IO.Pipelines;
8+
using System.Runtime.InteropServices;
89
using Microsoft.AspNetCore.Internal;
910

1011
namespace Microsoft.AspNetCore.WebUtilities;
@@ -271,6 +272,14 @@ private void EnsureFileStream()
271272
{
272273
var tempFileDirectory = _tempFileDirectoryAccessor();
273274
var tempFileName = Path.Combine(tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid() + ".tmp");
275+
276+
// Create a temp file with the correct Unix file mode before moving it to the assigned tempFileName in the _tempFileDirectory.
277+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
278+
{
279+
var tempTempFileName = Path.GetTempFileName();
280+
File.Move(tempTempFileName, tempFileName);
281+
}
282+
274283
FileStream = new FileStream(
275284
tempFileName,
276285
FileMode.Create,

src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers;
5+
using Microsoft.AspNetCore.Testing;
56
using Moq;
67

78
namespace Microsoft.AspNetCore.WebUtilities;
@@ -599,6 +600,33 @@ public async Task PartialReadAsyncThenSeekReplaysBuffer()
599600
Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray());
600601
}
601602

603+
[ConditionalFact]
604+
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")]
605+
public void Read_BufferingContentToDisk_CreatesFileWithUserOnlyUnixFileMode()
606+
{
607+
var inner = MakeStream(1024 * 2);
608+
string tempFileName;
609+
using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory()))
610+
{
611+
var bytes = new byte[1024 * 2];
612+
var read0 = stream.Read(bytes, 0, bytes.Length);
613+
Assert.Equal(bytes.Length, read0);
614+
Assert.Equal(read0, stream.Length);
615+
Assert.Equal(read0, stream.Position);
616+
Assert.False(stream.InMemory);
617+
Assert.NotNull(stream.TempFileName);
618+
619+
var read1 = stream.Read(bytes, 0, bytes.Length);
620+
Assert.Equal(0, read1);
621+
622+
tempFileName = stream.TempFileName!;
623+
Assert.True(File.Exists(tempFileName));
624+
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(tempFileName));
625+
}
626+
627+
Assert.False(File.Exists(tempFileName));
628+
}
629+
602630
private static string GetCurrentDirectory()
603631
{
604632
return AppContext.BaseDirectory;

src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers;
55
using System.Text;
6+
using Microsoft.AspNetCore.Testing;
67

78
namespace Microsoft.AspNetCore.WebUtilities;
89

@@ -365,6 +366,23 @@ public async Task DrainBufferAsync_WithContentInDisk_CopiesContentFromMemoryStre
365366
Assert.Equal(0, bufferingStream.Length);
366367
}
367368

369+
[ConditionalFact]
370+
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")]
371+
public void Write_BufferingContentToDisk_CreatesFileWithUserOnlyUnixFileMode()
372+
{
373+
// Arrange
374+
var input = new byte[] { 1, 2, 3, };
375+
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
376+
bufferingStream.Write(input, 0, 2);
377+
378+
// Act
379+
bufferingStream.Write(input, 2, 1);
380+
381+
// Assert
382+
Assert.NotNull(bufferingStream.FileStream);
383+
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(bufferingStream.FileStream.SafeFileHandle));
384+
}
385+
368386
public void Dispose()
369387
{
370388
try

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Diagnostics.Tracing;
77
using System.Linq;
8+
using System.Runtime.InteropServices;
89
using System.Security.Cryptography;
910
using System.Security.Cryptography.X509Certificates;
1011
using System.Text;
@@ -548,6 +549,14 @@ internal static void ExportCertificate(X509Certificate2 certificate, string path
548549
try
549550
{
550551
Log.WriteCertificateToDisk(path);
552+
553+
// Create a temp file with the correct Unix file mode before moving it to the expected path.
554+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
555+
{
556+
var tempFilename = Path.GetTempFileName();
557+
File.Move(tempFilename, path, overwrite: true);
558+
}
559+
551560
File.WriteAllBytes(path, bytes);
552561
}
553562
catch (Exception ex) when (Log.IsEnabled())
@@ -568,6 +577,14 @@ internal static void ExportCertificate(X509Certificate2 certificate, string path
568577
{
569578
var keyPath = Path.ChangeExtension(path, ".key");
570579
Log.WritePemKeyToDisk(keyPath);
580+
581+
// Create a temp file with the correct Unix file mode before moving it to the expected path.
582+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
583+
{
584+
var tempFilename = Path.GetTempFileName();
585+
File.Move(tempFilename, keyPath, overwrite: true);
586+
}
587+
571588
File.WriteAllBytes(keyPath, pemEnvelope);
572589
}
573590
catch (Exception ex) when (Log.IsEnabled())

src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,32 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
418418
e.Oid.Value == CertificateManager.AspNetHttpsOid &&
419419
e.RawData[0] == 1);
420420
}
421+
422+
[ConditionalFact]
423+
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")]
424+
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "https://github.com/dotnet/aspnetcore/issues/6720")]
425+
public void EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode()
426+
{
427+
_fixture.CleanupCertificates();
428+
429+
const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode) + ".pem";
430+
const string KeyName = nameof(EnsureCreateHttpsCertificate_CreatesFilesWithUserOnlyUnixFileMode) + ".key";
431+
432+
var certificatePassword = Guid.NewGuid().ToString();
433+
var now = DateTimeOffset.UtcNow;
434+
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
435+
436+
var result = _manager
437+
.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false);
438+
439+
Assert.Equal(EnsureCertificateResult.Succeeded, result);
440+
441+
Assert.True(File.Exists(CertificateName));
442+
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(CertificateName));
443+
444+
Assert.True(File.Exists(KeyName));
445+
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(KeyName));
446+
}
421447
}
422448

423449
public class CertFixture : IDisposable

src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal sealed class CreateCommand
2020
@"s\s"
2121
};
2222

23-
public static void Register(ProjectCommandLineApplication app)
23+
public static void Register(ProjectCommandLineApplication app, Program program)
2424
{
2525
app.Command("create", cmd =>
2626
{
@@ -94,7 +94,7 @@ public static void Register(ProjectCommandLineApplication app)
9494
return 1;
9595
}
9696

97-
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, outputOption.Value());
97+
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString, outputOption.Value(), program);
9898
});
9999
});
100100
}
@@ -227,7 +227,8 @@ private static int Execute(
227227
string projectPath,
228228
JwtCreatorOptions options,
229229
string optionsString,
230-
string outputFormat)
230+
string outputFormat,
231+
Program program)
231232
{
232233
if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
233234
{
@@ -238,7 +239,7 @@ private static int Execute(
238239
var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial);
239240
var jwtToken = jwtIssuer.Create(options);
240241

241-
var jwtStore = new JwtStore(userSecretsId);
242+
var jwtStore = new JwtStore(userSecretsId, program);
242243
var jwt = Jwt.Create(options.Scheme, jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims);
243244
if (options.Claims is { } customClaims)
244245
{

src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Runtime.InteropServices;
45
using System.Text.Json;
56
using Microsoft.Extensions.Configuration.UserSecrets;
67

@@ -12,11 +13,17 @@ public class JwtStore
1213
private readonly string _userSecretsId;
1314
private readonly string _filePath;
1415

15-
public JwtStore(string userSecretsId)
16+
public JwtStore(string userSecretsId, Program program = null)
1617
{
1718
_userSecretsId = userSecretsId;
1819
_filePath = Path.Combine(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(userSecretsId)), FileName);
1920
Load();
21+
22+
// For testing.
23+
if (program is not null)
24+
{
25+
program.UserJwtsFilePath = _filePath;
26+
}
2027
}
2128

2229
public IDictionary<string, Jwt> Jwts { get; private set; } = new Dictionary<string, Jwt>();
@@ -37,6 +44,13 @@ public void Save()
3744
{
3845
if (Jwts is not null)
3946
{
47+
// Create a temp file with the correct Unix file mode before moving it to the expected _filePath.
48+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
49+
{
50+
var tempFilename = Path.GetTempFileName();
51+
File.Move(tempFilename, _filePath, overwrite: true);
52+
}
53+
4054
using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write);
4155
JsonSerializer.Serialize(fileStream, Jwts);
4256
}

0 commit comments

Comments
 (0)