Skip to content

Commit

Permalink
Merged PR 25951: [internal/release/7.0] Use Path.GetTempFileName() fo…
Browse files Browse the repository at this point in the history
…r 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 <[email protected]>
  • Loading branch information
wtgodbe and mmitche authored Oct 17, 2022
1 parent 30d6813 commit 0c3eabf
Show file tree
Hide file tree
Showing 15 changed files with 205 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<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>
Expand Down
9 changes: 9 additions & 0 deletions src/Http/WebUtilities/src/FileBufferingReadStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/Http/WebUtilities/src/FileBufferingWriteStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers;
using System.Text;
using Microsoft.AspNetCore.Testing;

namespace Microsoft.AspNetCore.WebUtilities;

Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
Expand All @@ -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())
Expand Down
26 changes: 26 additions & 0 deletions src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
{
Expand Down Expand Up @@ -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);
});
});
}
Expand Down Expand Up @@ -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))
{
Expand All @@ -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)
{
Expand Down
16 changes: 15 additions & 1 deletion src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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>();
Expand All @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion src/Tools/dotnet-user-jwts/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
11 changes: 11 additions & 0 deletions src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<KeyValuePair<string, string>> AsEnumerable() => _secrets;
Expand Down Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit 0c3eabf

Please sign in to comment.