Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
<ItemGroup Condition="'$(TargetFramework)' != 'net472'">
<Compile Remove="Tasks/CreateNewImageToolTask.cs" />
<Compile Remove="net472Definitions.cs" />
<Compile Remove="VSHostObject.cs" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Strings.Designer.cs">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,38 @@ internal async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
return !Log.HasLoggedErrors;
}

bool credentialsSet = false;
VSHostObject hostObj = new(HostObject, Log);
if (hostObj.TryGetCredentials() is (string userName, string pass))
{
// Set credentials for the duration of this operation.
// These will be cleared in the finally block to minimize exposure.
Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, userName);
Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, pass);
credentialsSet = true;
}
else
{
Log.LogMessage(MessageImportance.Low, Resource.GetString(nameof(Strings.HostObjectNotDetected)));
}

try
{
return await ExecuteAsyncCore(logger, msbuildLoggerFactory, cancellationToken).ConfigureAwait(false);
}
finally
{
// Clear credentials from environment to minimize exposure window.
if (credentialsSet)
{
Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, null);
Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, null);
}
}
}

private async Task<bool> ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuildLoggerFactory, CancellationToken cancellationToken)
{
RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull;
Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode);
SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ private string DotNetPath
/// <returns></returns>
protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch)
{
VSHostObject hostObj = new(HostObject as System.Collections.Generic.IEnumerable<ITaskItem>);
if (hostObj.ExtractCredentials(out string user, out string pass, (string s) => Log.LogWarning(s)))
VSHostObject hostObj = new(HostObject, Log);
if (hostObj.TryGetCredentials() is (string user, string pass))
{
extractionInfo = (true, user, pass);
}
Expand Down
111 changes: 94 additions & 17 deletions src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,121 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Text.Json;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.NET.Build.Containers.Tasks;

internal sealed class VSHostObject
internal sealed class VSHostObject(ITaskHost? hostObject, TaskLoggingHelper log)
{
private const string CredentialItemSpecName = "MsDeployCredential";
private const string UserMetaDataName = "UserName";
private const string PasswordMetaDataName = "Password";
IEnumerable<ITaskItem>? _hostObject;

public VSHostObject(IEnumerable<ITaskItem>? hostObject)
private readonly ITaskHost? _hostObject = hostObject;
private readonly TaskLoggingHelper _log = log;

/// <summary>
/// Tries to extract credentials from the host object.
/// </summary>
/// <returns>A tuple of (username, password) if credentials were found with non-empty username, null otherwise.</returns>
public (string username, string password)? TryGetCredentials()
{
_hostObject = hostObject;
if (_hostObject is null)
{
return null;
}

IEnumerable<ITaskItem>? taskItems = GetTaskItems();
if (taskItems is null)
{
_log.LogMessage(MessageImportance.Low, "No task items found in host object.");
return null;
}

ITaskItem? credentialItem = taskItems.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName);
if (credentialItem is null)
{
return null;
}

string username = credentialItem.GetMetadata(UserMetaDataName);
if (string.IsNullOrEmpty(username))
{
return null;
}

string password = credentialItem.GetMetadata(PasswordMetaDataName);
return (username, password);
}

public bool ExtractCredentials(out string username, out string password, Action<string> logMethod)
private IEnumerable<ITaskItem>? GetTaskItems()
{
bool retVal = false;
username = password = string.Empty;
if (_hostObject != null)
try
{
ITaskItem credentialItem = _hostObject.FirstOrDefault<ITaskItem>(p => p.ItemSpec == CredentialItemSpecName);
if (credentialItem != null)
// This call mirrors the behavior of Microsoft.WebTools.Publish.MSDeploy.VSMsDeployTaskHostObject.QueryAllTaskItems.
// Expected contract:
// - Instance method on the host object named "QueryAllTaskItems".
// - Signature: string QueryAllTaskItems().
// - Returns a JSON array of objects with the shape:
// [{ "ItemSpec": "<string>", "Metadata": { "<key>": "<value>", ... } }, ...]
// The JSON is deserialized into TaskItemDto records and converted to ITaskItem instances.
// Only UserName and Password metadata are extracted to avoid conflicts with reserved MSBuild metadata.
string? rawTaskItems = (string?)_hostObject!.GetType().InvokeMember(
"QueryAllTaskItems",
BindingFlags.InvokeMethod,
null,
_hostObject,
null);

if (!string.IsNullOrEmpty(rawTaskItems))
{
retVal = true;
username = credentialItem.GetMetadata(UserMetaDataName);
if (!string.IsNullOrEmpty(username))
List<TaskItemDto>? dtos = JsonSerializer.Deserialize<List<TaskItemDto>>(rawTaskItems);
if (dtos is not null && dtos.Count > 0)
{
password = credentialItem.GetMetadata(PasswordMetaDataName);
_log.LogMessage(MessageImportance.Low, "Successfully retrieved task items via QueryAllTaskItems.");
return dtos.Select(ConvertToTaskItem).ToList();
}
else
}

_log.LogMessage(MessageImportance.Low, "QueryAllTaskItems returned null or empty result.");
}
catch (Exception ex)
{
_log.LogMessage(MessageImportance.Low, "Exception trying to call QueryAllTaskItems: {0}", ex.Message);
}

// Fallback: try to use the host object directly as IEnumerable<ITaskItem> (legacy behavior).
if (_hostObject is IEnumerable<ITaskItem> enumerableHost)
{
_log.LogMessage(MessageImportance.Low, "Falling back to IEnumerable<ITaskItem> host object.");
return enumerableHost;
}

return null;

static TaskItem ConvertToTaskItem(TaskItemDto dto)
{
TaskItem taskItem = new(dto.ItemSpec ?? string.Empty);
if (dto.Metadata is not null)
{
if (dto.Metadata.TryGetValue(UserMetaDataName, out string? userName))
{
logMethod("HostObject credentials not detected. Falling back to Docker credential retrieval.");
taskItem.SetMetadata(UserMetaDataName, userName);
}

if (dto.Metadata.TryGetValue(PasswordMetaDataName, out string? password))
{
taskItem.SetMetadata(PasswordMetaDataName, password);
}
}

return taskItem;
}
return retVal;
}

private readonly record struct TaskItemDto(string? ItemSpec, Dictionary<string, string>? Metadata);
}

Loading