From 00034f22249c041add1136c4fb24b1df8857fd89 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 14 Jul 2025 12:08:09 -0500 Subject: [PATCH] allow reading uid from ContainerUser to set the Tar entry uid --- .../ContainerBuilder.cs | 22 +++++++++- .../Microsoft.NET.Build.Containers/Layer.cs | 41 ++++++++++++------- .../Tasks/CreateNewImage.cs | 4 +- .../LayerEndToEndTests.cs | 20 +++++++++ 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs index 9859e15d5179..e8153bb32314 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -114,8 +114,8 @@ internal static async Task ContainerizeAsync( KnownImageFormats.OCI => SchemaTypes.OciManifestV1, _ => imageBuilder.ManifestMediaType // should be impossible unless we add to the enum }; - - Layer newLayer = Layer.FromDirectory(publishDirectory.FullName, workingDir, imageBuilder.IsWindows, imageBuilder.ManifestMediaType); + var userId = imageBuilder.IsWindows ? null : TryParseUserId(containerUser); + Layer newLayer = Layer.FromDirectory(publishDirectory.FullName, workingDir, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId); imageBuilder.AddLayer(newLayer); imageBuilder.SetWorkingDirectory(workingDir); @@ -200,6 +200,24 @@ internal static async Task ContainerizeAsync( return exitCode; } + public static int? TryParseUserId(string? containerUser) + { + if (containerUser is null) + { + return null; + } + if (int.TryParse(containerUser, out int userId)) + { + return userId; + } + if (containerUser.Equals("root", StringComparison.OrdinalIgnoreCase)) + { + return 0; // root user + } + // TODO: on Linux we could _potentially_ try to map the user name to a UID + return null; + } + private static async Task PushToLocalRegistryAsync(ILogger logger, BuiltImage builtImage, SourceImageReference sourceImageReference, DestinationImageReference destinationImageReference, CancellationToken cancellationToken) diff --git a/src/Containers/Microsoft.NET.Build.Containers/Layer.cs b/src/Containers/Microsoft.NET.Build.Containers/Layer.cs index fc97038017c2..1cb57628f390 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Layer.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Layer.cs @@ -12,7 +12,9 @@ namespace Microsoft.NET.Build.Containers; internal class Layer { - // NOTE: The SID string below was created using the following snippet. As the code is Windows only we keep the constant + // NOTE: The SID string below was created using the following snippet. As the code is Windows only we keep the constant, + // so that we can author Windows layers successfully on non-Windows hosts. + // // private static string CreateUserOwnerAndGroupSID() // { // var descriptor = new RawSecurityDescriptor( @@ -50,7 +52,7 @@ public static Layer FromDescriptor(Descriptor descriptor) return new(ContentStore.PathForDescriptor(descriptor), descriptor); } - public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer, string manifestMediaType) + public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer, string manifestMediaType, int? userId = null) { long fileSize; Span hash = stackalloc byte[SHA256.HashSizeInBytes]; @@ -101,7 +103,7 @@ public static Layer FromDirectory(string directory, string containerPath, bool i } // Write an entry for the application directory. - WriteTarEntryForFile(writer, new DirectoryInfo(directory), containerPath, entryAttributes); + WriteTarEntryForFile(writer, new DirectoryInfo(directory), containerPath, entryAttributes, isWindowsLayer ? null : userId); // Write entries for the application directory contents. var fileList = new FileSystemEnumerable<(FileSystemInfo file, string containerPath)>( @@ -124,7 +126,7 @@ public static Layer FromDirectory(string directory, string containerPath, bool i }); foreach (var item in fileList) { - WriteTarEntryForFile(writer, item.file, item.containerPath, entryAttributes); + WriteTarEntryForFile(writer, item.file, item.containerPath, entryAttributes, isWindowsLayer ? null : userId); } // Windows layers need a Hives folder, we do not need to create any Registry Hive deltas inside @@ -148,27 +150,36 @@ public static Layer FromDirectory(string directory, string containerPath, bool i Debug.Assert(bW == hash.Length); // Writes a tar entry corresponding to the file system item. - static void WriteTarEntryForFile(TarWriter writer, FileSystemInfo file, string containerPath, IEnumerable> entryAttributes) + static void WriteTarEntryForFile(TarWriter writer, FileSystemInfo file, string containerPath, IEnumerable> entryAttributes, int? userId) { UnixFileMode mode = DetermineFileMode(file); + PaxTarEntry entry; if (file is FileInfo) { - using var fileStream = File.OpenRead(file.FullName); - PaxTarEntry entry = new(TarEntryType.RegularFile, containerPath, entryAttributes) + var fileStream = File.OpenRead(file.FullName); + entry = new(TarEntryType.RegularFile, containerPath, entryAttributes) { - Mode = mode, - DataStream = fileStream + DataStream = fileStream, }; - writer.WriteEntry(entry); } else { - PaxTarEntry entry = new(TarEntryType.Directory, containerPath, entryAttributes) - { - Mode = mode - }; - writer.WriteEntry(entry); + entry = new(TarEntryType.Directory, containerPath, entryAttributes); + } + + entry.Mode = mode; + if (userId is int uid) + { + entry.Uid = uid; + } + + writer.WriteEntry(entry); + + if (entry.DataStream is not null) + { + // no longer relying on the `using` of the FileStream, so need to do it manually + entry.DataStream.Dispose(); } static UnixFileMode DetermineFileMode(FileSystemInfo file) diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index dd33472c27f5..777ed43ee10f 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -160,8 +160,8 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) Log.LogErrorWithCodeFromResources(nameof(Strings.InvalidContainerImageFormat), ImageFormat, string.Join(",", Enum.GetValues())); } } - - Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows, imageBuilder.ManifestMediaType); + var userId = imageBuilder.IsWindows ? null : ContainerBuilder.TryParseUserId(ContainerUser); + Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows, imageBuilder.ManifestMediaType, userId); imageBuilder.AddLayer(newLayer); imageBuilder.SetWorkingDirectory(WorkingDirectory); diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs index b0a23aabf122..9e5bcd22d71d 100644 --- a/test/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs +++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/LayerEndToEndTests.cs @@ -99,6 +99,26 @@ public void SingleFileInHiddenFolder() Assert.True(allEntries.TryGetValue("app/wwwroot/.well-known/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing app/wwwroot/.well-known/TestFile.txt file entry"); } + [Fact] + public void UserIdIsAppliedToFiles() + { + using TransientTestFolder folder = new(); + + string testFilePath = Path.Join(folder.Path, "TestFile.txt"); + string testString = $"Test content for {nameof(SingleFileInFolder)}"; + File.WriteAllText(testFilePath, testString); + + var userId = 1234; + Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "/app", false, SchemaTypes.DockerManifestV2, userId: userId); + var allEntries = LoadAllTarEntries(l.BackingFile); + Assert.True(allEntries.TryGetValue("app", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing app directory entry"); + Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry"); + Assert.All(allEntries.Values, entry => + { + Assert.True(entry.Uid == userId, $"Expected UID {userId} for entry {entry.Name}, but got {entry.Uid}"); + }); + } + private static void VerifyDescriptorInfo(Layer l) { Assert.Equal(l.Descriptor.Size, new FileInfo(l.BackingFile).Length);