diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index cfd811544..491e5802b 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2281,6 +2281,12 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Pulls images.
+
+ Upload an image to a registry.
+
+
+ Upload an image to a registry.
+
Remove images.
@@ -2293,6 +2299,71 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Saves images.
+
+ Log in to a registry.
+
+
+ Log in to a registry. If no server is specified, the default is defined by the session.
+
+
+ Log out from a registry.
+
+
+ Log out from a registry. If no server is specified, the default is defined by the session.
+
+
+ Login Succeeded
+
+
+ Removing login credentials for {}
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Not logged in to {}
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Failed to parse credentials file '{}': the file may be corrupted.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Failed to write '{}': {}
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+
+ Server
+
+
+ Username
+
+
+ Password or Personal Access Token (PAT)
+ {Locked="PAT"}Acronym should not be translated
+
+
+ Take the Password or Personal Access Token (PAT) from stdin
+ {Locked="PAT"}{Locked="stdin"}Technical terms should not be translated
+
+
+ --password and --password-stdin are mutually exclusive
+ {Locked="--password "}{Locked="--password-stdin "}Command line arguments, file names and string inserts should not be translated
+
+
+ Must provide --username with --password-stdin
+ {Locked="--username "}{Locked="--password-stdin"}Command line arguments, file names and string inserts should not be translated
+
+
+ Username:
+
+
+ Password:
+
+
+ Manage registry credentials.
+
+
+ Manage registry credentials, including logging in and out of container registries.
+
Tag an image.
diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp
index 53ce52cf3..78eb56403 100644
--- a/src/windows/common/WSLCContainerLauncher.cpp
+++ b/src/windows/common/WSLCContainerLauncher.cpp
@@ -11,6 +11,7 @@ Module Name:
This file contains the implementation for WSLCContainerLauncher.
--*/
+
#include "precomp.h"
#include "WSLCContainerLauncher.h"
diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp
index 977f03635..af26c9b3d 100644
--- a/src/windows/common/WSLCUserSettings.cpp
+++ b/src/windows/common/WSLCUserSettings.cpp
@@ -49,7 +49,10 @@ static constexpr std::string_view s_DefaultSettingsTemplate =
" # Default path for session storage. By default, storage is per-session under:\n"
" # %LocalAppData%\\wslc\\sessions\\wslc-cli (standard sessions)\n"
" # %LocalAppData%\\wslc\\sessions\\wslc-cli-admin (elevated sessions)\n"
- " # defaultStoragePath: \"\"\n";
+ " # defaultStoragePath: \"\"\n"
+ "\n"
+ "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n"
+ "# credentialStore: wincred\n";
// Validate individual setting specializations
namespace details {
@@ -123,6 +126,20 @@ namespace details {
return value;
}
+ WSLC_VALIDATE_SETTING(CredentialStore)
+ {
+ if (value == "wincred")
+ {
+ return CredentialStoreType::WinCred;
+ }
+ if (value == "file")
+ {
+ return CredentialStoreType::File;
+ }
+
+ return std::nullopt;
+ }
+
#undef WSLC_VALIDATE_SETTING
} // namespace details
diff --git a/src/windows/common/WSLCUserSettings.h b/src/windows/common/WSLCUserSettings.h
index d47da6b5e..f0201ca22 100644
--- a/src/windows/common/WSLCUserSettings.h
+++ b/src/windows/common/WSLCUserSettings.h
@@ -42,6 +42,7 @@ enum class Setting : size_t
SessionNetworkingMode,
SessionHostFileShareMode,
SessionDnsTunneling,
+ CredentialStore,
Max
};
@@ -52,6 +53,12 @@ enum class HostFileShareMode
VirtioFs
};
+enum class CredentialStoreType
+{
+ WinCred,
+ File
+};
+
namespace details {
template
@@ -83,6 +90,7 @@ namespace details {
DEFINE_SETTING_MAPPING(SessionNetworkingMode, std::string, WSLCNetworkingMode, WSLCNetworkingModeVirtioProxy, "session.networkingMode")
DEFINE_SETTING_MAPPING(SessionHostFileShareMode, std::string, HostFileShareMode, HostFileShareMode::VirtioFs, "session.hostFileShareMode")
DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling")
+ DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
#undef DEFINE_SETTING_MAPPING
// clang-format on
diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp
index 318374cca..b1299a24d 100644
--- a/src/windows/common/wslutil.cpp
+++ b/src/windows/common/wslutil.cpp
@@ -1448,17 +1448,15 @@ std::string wsl::windows::common::wslutil::Base64Decode(const std::string& encod
return result;
}
-std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress)
+std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password)
{
- nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}};
-
+ nlohmann::json authJson = {{"username", username}, {"password", password}};
return Base64Encode(authJson.dump());
}
-std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress)
+std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken)
{
- nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}};
-
+ nlohmann::json authJson = {{"identitytoken", identityToken}};
return Base64Encode(authJson.dump());
}
@@ -1484,4 +1482,4 @@ std::map wsl::windows::common::wslutil::ParseKeyValueP
}
return result;
-}
\ No newline at end of file
+}
diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h
index 63663e573..25d5f5b87 100644
--- a/src/windows/common/wslutil.h
+++ b/src/windows/common/wslutil.h
@@ -335,11 +335,11 @@ std::string Base64Decode(const std::string& encoded);
// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs
// (PullImage, PushImage, etc.) from the given credentials.
-std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress);
+std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password);
// Builds the base64-encoded X-Registry-Auth header value from an identity token
// returned by Authenticate().
-std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress);
+std::string BuildRegistryAuthHeader(const std::string& identityToken);
std::map ParseKeyValuePairs(_In_reads_opt_(count) const KeyValuePair* pairs, ULONG count, _In_opt_ LPCSTR reservedKey = nullptr);
diff --git a/src/windows/wslc/CMakeLists.txt b/src/windows/wslc/CMakeLists.txt
index b1552aa9a..3988c1da1 100644
--- a/src/windows/wslc/CMakeLists.txt
+++ b/src/windows/wslc/CMakeLists.txt
@@ -14,7 +14,10 @@ target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUB
target_link_libraries(wslclib
${COMMON_LINK_LIBRARIES}
yaml-cpp
- common)
+ common
+ advapi32
+ crypt32)
+
target_precompile_headers(wslclib REUSE_FROM common)
set_target_properties(wslclib PROPERTIES FOLDER windows)
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index 32d74f832..cdde9b05e 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -64,6 +64,8 @@ _(NoCache, "no-cache", NO_ALIAS, Kind::Flag, L
_(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \
_(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \
_(Output, "output", L"o", Kind::Value, Localization::WSLCCLI_OutputArgDescription()) \
+_(Password, "password", L"p", Kind::Value, Localization::WSLCCLI_LoginPasswordArgDescription()) \
+_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \
_(Path, "path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_PathArgDescription()) \
/*_(Progress, "progress", NO_ALIAS, Kind::Value, Localization::WSLCCLI_ProgressArgDescription())*/ \
_(Publish, "publish", L"p", Kind::Value, Localization::WSLCCLI_PublishArgDescription()) \
@@ -71,6 +73,7 @@ _(Publish, "publish", L"p", Kind::Value, L
_(Quiet, "quiet", L"q", Kind::Flag, Localization::WSLCCLI_QuietArgDescription()) \
_(Remove, "rm", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_RemoveArgDescription()) \
/*_(Scheme, "scheme", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SchemeArgDescription())*/ \
+_(Server, "server", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_LoginServerArgDescription()) \
_(Session, "session", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SessionIdArgDescription()) \
_(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
@@ -82,6 +85,7 @@ _(Time, "time", L"t", Kind::Value, L
_(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \
_(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \
_(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \
+_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \
_(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \
_(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \
/*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \
diff --git a/src/windows/wslc/commands/ImageCommand.cpp b/src/windows/wslc/commands/ImageCommand.cpp
index 1583f704f..c229fd057 100644
--- a/src/windows/wslc/commands/ImageCommand.cpp
+++ b/src/windows/wslc/commands/ImageCommand.cpp
@@ -28,6 +28,7 @@ std::vector> ImageCommand::GetCommands() const
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
return commands;
diff --git a/src/windows/wslc/commands/ImageCommand.h b/src/windows/wslc/commands/ImageCommand.h
index 6b0aef21b..f574a2796 100644
--- a/src/windows/wslc/commands/ImageCommand.h
+++ b/src/windows/wslc/commands/ImageCommand.h
@@ -146,6 +146,21 @@ struct ImagePullCommand final : public Command
void ExecuteInternal(CLIExecutionContext& context) const override;
};
+// Push Command
+struct ImagePushCommand final : public Command
+{
+ constexpr static std::wstring_view CommandName = L"push";
+ ImagePushCommand(const std::wstring& parent) : Command(CommandName, parent)
+ {
+ }
+ std::vector GetArguments() const override;
+ std::wstring ShortDescription() const override;
+ std::wstring LongDescription() const override;
+
+protected:
+ void ExecuteInternal(CLIExecutionContext& context) const override;
+};
+
// Save Command
struct ImageSaveCommand final : public Command
{
diff --git a/src/windows/wslc/commands/ImagePushCommand.cpp b/src/windows/wslc/commands/ImagePushCommand.cpp
new file mode 100644
index 000000000..8db9d28cf
--- /dev/null
+++ b/src/windows/wslc/commands/ImagePushCommand.cpp
@@ -0,0 +1,51 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ ImagePushCommand.cpp
+
+Abstract:
+
+ Implementation of the image push command.
+
+--*/
+
+#include "ImageCommand.h"
+#include "CLIExecutionContext.h"
+#include "ImageTasks.h"
+#include "SessionTasks.h"
+#include "Task.h"
+
+using namespace wsl::windows::wslc::execution;
+using namespace wsl::windows::wslc::task;
+using namespace wsl::shared;
+
+namespace wsl::windows::wslc {
+// Image Push Command
+std::vector ImagePushCommand::GetArguments() const
+{
+ return {
+ Argument::Create(ArgType::ImageId, true),
+ Argument::Create(ArgType::Session),
+ };
+}
+
+std::wstring ImagePushCommand::ShortDescription() const
+{
+ return Localization::WSLCCLI_ImagePushDesc();
+}
+
+std::wstring ImagePushCommand::LongDescription() const
+{
+ return Localization::WSLCCLI_ImagePushLongDesc();
+}
+
+void ImagePushCommand::ExecuteInternal(CLIExecutionContext& context) const
+{
+ context //
+ << CreateSession //
+ << PushImage;
+}
+} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp
new file mode 100644
index 000000000..16a207d7b
--- /dev/null
+++ b/src/windows/wslc/commands/RegistryCommand.cpp
@@ -0,0 +1,182 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryCommand.cpp
+
+Abstract:
+
+ Implementation of the registry command tree (login, logout).
+
+--*/
+
+#include "CLIExecutionContext.h"
+#include "RegistryCommand.h"
+#include "RegistryTasks.h"
+#include "SessionTasks.h"
+#include "Task.h"
+#include
+
+using namespace wsl::windows::wslc::execution;
+using namespace wsl::windows::wslc::task;
+using namespace wsl::shared;
+
+namespace {
+
+auto MaskInput()
+{
+ HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD mode = 0;
+
+ if ((input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode))
+ {
+ THROW_IF_WIN32_BOOL_FALSE(SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT));
+ return wil::scope_exit(std::function([input, mode] {
+ SetConsoleMode(input, mode);
+ std::wcerr << L'\n';
+ }));
+ }
+
+ return wil::scope_exit(std::function([] {}));
+}
+
+std::wstring Prompt(const std::wstring& label, bool maskInput)
+{
+ // Write without a trailing newline so the cursor stays inline (matching Docker's behavior).
+ std::wcerr << label;
+
+ auto restoreConsole = maskInput ? MaskInput() : wil::scope_exit(std::function([] {}));
+
+ std::wstring value;
+ std::getline(std::wcin, value);
+
+ return value;
+}
+
+} // namespace
+
+namespace wsl::windows::wslc {
+
+// Registry Root Command
+std::vector> RegistryCommand::GetCommands() const
+{
+ std::vector> commands;
+ commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
+ return commands;
+}
+
+std::vector RegistryCommand::GetArguments() const
+{
+ return {};
+}
+
+std::wstring RegistryCommand::ShortDescription() const
+{
+ return Localization::WSLCCLI_RegistryCommandDesc();
+}
+
+std::wstring RegistryCommand::LongDescription() const
+{
+ return Localization::WSLCCLI_RegistryCommandLongDesc();
+}
+
+void RegistryCommand::ExecuteInternal(CLIExecutionContext& context) const
+{
+ OutputHelp();
+}
+
+// Registry Login Command
+std::vector RegistryLoginCommand::GetArguments() const
+{
+ return {
+ Argument::Create(ArgType::Password),
+ Argument::Create(ArgType::PasswordStdin),
+ Argument::Create(ArgType::Username),
+ Argument::Create(ArgType::Server),
+ Argument::Create(ArgType::Session),
+ };
+}
+
+std::wstring RegistryLoginCommand::ShortDescription() const
+{
+ return Localization::WSLCCLI_LoginDesc();
+}
+
+std::wstring RegistryLoginCommand::LongDescription() const
+{
+ return Localization::WSLCCLI_LoginLongDesc();
+}
+
+void RegistryLoginCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
+{
+ if (execArgs.Contains(ArgType::Password) && execArgs.Contains(ArgType::PasswordStdin))
+ {
+ throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive());
+ }
+
+ if (execArgs.Contains(ArgType::PasswordStdin) && !execArgs.Contains(ArgType::Username))
+ {
+ throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername());
+ }
+}
+
+void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const
+{
+ // Prompt for username if not provided.
+ if (!context.Args.Contains(ArgType::Username))
+ {
+ context.Args.Add(ArgType::Username, Prompt(Localization::WSLCCLI_LoginUsernamePrompt(), false));
+ }
+
+ // Resolve password: --password, --password-stdin, or interactive prompt.
+ if (!context.Args.Contains(ArgType::Password))
+ {
+ if (context.Args.Contains(ArgType::PasswordStdin))
+ {
+ std::wstring line;
+ std::getline(std::wcin, line);
+ if (!line.empty() && line.back() == L'\r')
+ {
+ line.pop_back();
+ }
+
+ context.Args.Add(ArgType::Password, std::move(line));
+ }
+ else
+ {
+ context.Args.Add(ArgType::Password, Prompt(Localization::WSLCCLI_LoginPasswordPrompt(), true));
+ }
+ }
+
+ context //
+ << CreateSession << Login;
+}
+
+// Registry Logout Command
+std::vector RegistryLogoutCommand::GetArguments() const
+{
+ return {
+ Argument::Create(ArgType::Server),
+ };
+}
+
+std::wstring RegistryLogoutCommand::ShortDescription() const
+{
+ return Localization::WSLCCLI_LogoutDesc();
+}
+
+std::wstring RegistryLogoutCommand::LongDescription() const
+{
+ return Localization::WSLCCLI_LogoutLongDesc();
+}
+
+void RegistryLogoutCommand::ExecuteInternal(CLIExecutionContext& context) const
+{
+ context //
+ << Logout;
+}
+
+} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/RegistryCommand.h b/src/windows/wslc/commands/RegistryCommand.h
new file mode 100644
index 000000000..ed8d04e3e
--- /dev/null
+++ b/src/windows/wslc/commands/RegistryCommand.h
@@ -0,0 +1,71 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryCommand.h
+
+Abstract:
+
+ Declaration of the registry command tree (login, logout).
+
+--*/
+#pragma once
+#include "Command.h"
+
+namespace wsl::windows::wslc {
+
+// Root registry command: wslc registry [login|logout]
+struct RegistryCommand final : public Command
+{
+ constexpr static std::wstring_view CommandName = L"registry";
+ RegistryCommand(const std::wstring& parent) : Command(CommandName, parent)
+ {
+ }
+
+ std::vector> GetCommands() const override;
+ std::vector GetArguments() const override;
+ std::wstring ShortDescription() const override;
+ std::wstring LongDescription() const override;
+
+protected:
+ void ExecuteInternal(CLIExecutionContext& context) const override;
+};
+
+// Login Command
+struct RegistryLoginCommand final : public Command
+{
+ constexpr static std::wstring_view CommandName = L"login";
+
+ RegistryLoginCommand(const std::wstring& parent) : Command(CommandName, parent)
+ {
+ }
+
+ std::vector GetArguments() const override;
+ std::wstring ShortDescription() const override;
+ std::wstring LongDescription() const override;
+
+protected:
+ void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
+ void ExecuteInternal(CLIExecutionContext& context) const override;
+};
+
+// Logout Command
+struct RegistryLogoutCommand final : public Command
+{
+ constexpr static std::wstring_view CommandName = L"logout";
+
+ RegistryLogoutCommand(const std::wstring& parent) : Command(CommandName, parent)
+ {
+ }
+
+ std::vector GetArguments() const override;
+ std::wstring ShortDescription() const override;
+ std::wstring LongDescription() const override;
+
+protected:
+ void ExecuteInternal(CLIExecutionContext& context) const override;
+};
+
+} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp
index 091bc1cdb..d92c563de 100644
--- a/src/windows/wslc/commands/RootCommand.cpp
+++ b/src/windows/wslc/commands/RootCommand.cpp
@@ -16,6 +16,7 @@ Module Name:
// Include all commands that parent to the root.
#include "ContainerCommand.h"
#include "ImageCommand.h"
+#include "RegistryCommand.h"
#include "SessionCommand.h"
#include "SettingsCommand.h"
#include "VersionCommand.h"
@@ -29,6 +30,7 @@ std::vector> RootCommand::GetCommands() const
std::vector> commands;
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
@@ -40,8 +42,11 @@ std::vector> RootCommand::GetCommands() const
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
+ commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName()));
commands.push_back(std::make_unique(FullName(), true));
commands.push_back(std::make_unique(FullName()));
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index 217796df0..28164d1c7 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -16,7 +16,7 @@ Module Name:
#include "ContainerService.h"
#include "ConsoleService.h"
#include "ImageService.h"
-#include "PullImageCallback.h"
+#include "ImageProgressCallback.h"
#include
#include
#include
@@ -117,7 +117,7 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& sessio
{
{
// Attempt to pull the image if not found
- PullImageCallback callback;
+ ImageProgressCallback callback;
PrintMessage(Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)), stderr);
ImageService imageService;
imageService.Pull(session, image, &callback);
diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp
new file mode 100644
index 000000000..3cf562c69
--- /dev/null
+++ b/src/windows/wslc/services/FileCredStorage.cpp
@@ -0,0 +1,255 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ FileCredStorage.cpp
+
+Abstract:
+
+ DPAPI-encrypted JSON file credential storage implementation.
+
+--*/
+
+#include "precomp.h"
+#include "FileCredStorage.h"
+
+using wsl::shared::Localization;
+
+using namespace wsl::shared;
+using namespace wsl::windows::common::wslutil;
+using namespace wsl::windows::wslc::services;
+
+namespace {
+
+std::filesystem::path GetFilePath()
+{
+ return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json";
+}
+
+wil::unique_file RetryOpenFileOnSharingViolation(const std::function& openFunc)
+{
+ try
+ {
+ return wsl::shared::retry::RetryWithTimeout(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() {
+ return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
+ });
+ }
+ catch (...)
+ {
+ auto result = wil::ResultFromCaughtException();
+ auto errorString = wsl::windows::common::wslutil::GetSystemErrorString(result);
+ THROW_HR_WITH_USER_ERROR(result, Localization::MessageWslcFailedToOpenFile(GetFilePath(), errorString));
+ }
+}
+
+wil::unique_file OpenFileExclusive()
+{
+ wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"r+b", _SH_DENYRW));
+ if (!f)
+ {
+ auto dosError = _doserrno;
+ if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
+ {
+ return nullptr;
+ }
+
+ THROW_WIN32_IF(dosError, dosError != 0);
+ THROW_HR(E_FAIL);
+ }
+
+ return f;
+}
+
+wil::unique_file CreateFileExclusive()
+{
+ auto filePath = GetFilePath();
+ std::filesystem::create_directories(filePath.parent_path());
+
+ using UniqueFd = wil::unique_any;
+
+ UniqueFd fd;
+ auto err = _wsopen_s(fd.addressof(), filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE);
+ if (err != 0)
+ {
+ auto dosError = _doserrno;
+ THROW_WIN32_IF(dosError, dosError != 0);
+ THROW_HR(E_FAIL);
+ }
+
+ wil::unique_file f(_fdopen(fd.get(), "r+b"));
+ if (!f)
+ {
+ auto dosError = _doserrno;
+ THROW_WIN32_IF(dosError, dosError != 0);
+ THROW_HR(E_FAIL);
+ }
+
+ fd.release();
+ return f;
+}
+
+wil::unique_file OpenFileShared()
+{
+ wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"rb", _SH_DENYWR));
+ if (!f)
+ {
+ auto dosError = _doserrno;
+ if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
+ {
+ return nullptr;
+ }
+
+ THROW_WIN32_IF(dosError, dosError != 0);
+ THROW_HR(E_FAIL);
+ }
+
+ return f;
+}
+
+CredentialFile ReadCredentialFile(FILE* f)
+{
+ WI_ASSERT(f != nullptr);
+
+ auto seekResult = fseek(f, 0, SEEK_SET);
+ THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), seekResult != 0);
+
+ // Handle newly created empty files (from CreateFileExclusive).
+ if (_filelengthi64(_fileno(f)) <= 0)
+ {
+ return {};
+ }
+
+ try
+ {
+ return nlohmann::json::parse(f).get();
+ }
+ catch (const nlohmann::json::exception&)
+ {
+ THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath()));
+ }
+}
+
+void WriteCredentialFile(FILE* f, const CredentialFile& data)
+{
+ auto error = fseek(f, 0, SEEK_SET);
+ THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), error != 0);
+
+ error = _chsize_s(_fileno(f), 0);
+ THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(error)), error != 0);
+
+ auto content = nlohmann::json(data).dump(2);
+ auto written = fwrite(content.data(), 1, content.size(), f);
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), written != content.size());
+}
+
+void ModifyFileStore(FILE* f, const std::function& modifier)
+{
+ auto data = ReadCredentialFile(f);
+
+ if (modifier(data))
+ {
+ WriteCredentialFile(f, data);
+ }
+}
+
+std::string Protect(const std::string& plaintext)
+{
+ DATA_BLOB input{};
+ input.cbData = static_cast(plaintext.size());
+ input.pbData = reinterpret_cast(const_cast(plaintext.data()));
+
+ DATA_BLOB output{};
+ THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
+ auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
+
+ return Base64Encode(std::string(reinterpret_cast(output.pbData), output.cbData));
+}
+
+std::string Unprotect(const std::string& cipherBase64)
+{
+ auto decoded = Base64Decode(cipherBase64);
+
+ DATA_BLOB input{};
+ input.cbData = static_cast(decoded.size());
+ input.pbData = reinterpret_cast(decoded.data());
+
+ DATA_BLOB output{};
+ THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
+ auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
+
+ return std::string(reinterpret_cast(output.pbData), output.cbData);
+}
+
+} // namespace
+
+namespace wsl::windows::wslc::services {
+
+void FileCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
+{
+ auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive);
+
+ ModifyFileStore(file.get(), [&](CredentialFile& data) {
+ data.Credentials[serverAddress] = CredentialEntry{username, Protect(secret)};
+ return true;
+ });
+}
+
+std::pair FileCredStorage::Get(const std::string& serverAddress)
+{
+ auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
+ if (!file)
+ {
+ return {};
+ }
+
+ auto data = ReadCredentialFile(file.get());
+ const auto entry = data.Credentials.find(serverAddress);
+
+ if (entry == data.Credentials.end())
+ {
+ return {};
+ }
+
+ return {entry->second.UserName, Unprotect(entry->second.Secret)};
+}
+
+void FileCredStorage::Erase(const std::string& serverAddress)
+{
+ auto file = RetryOpenFileOnSharingViolation(OpenFileExclusive);
+ bool erased = false;
+
+ if (file)
+ {
+ ModifyFileStore(file.get(), [&](CredentialFile& data) {
+ erased = data.Credentials.erase(serverAddress) > 0;
+ return erased;
+ });
+ }
+
+ THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased);
+}
+
+std::vector FileCredStorage::List()
+{
+ auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
+ if (!file)
+ {
+ return {};
+ }
+
+ auto data = ReadCredentialFile(file.get());
+
+ std::vector result;
+
+ for (const auto& [key, value] : data.Credentials)
+ {
+ result.push_back(wsl::shared::string::MultiByteToWide(key));
+ }
+
+ return result;
+}
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/FileCredStorage.h b/src/windows/wslc/services/FileCredStorage.h
new file mode 100644
index 000000000..5b03e7c84
--- /dev/null
+++ b/src/windows/wslc/services/FileCredStorage.h
@@ -0,0 +1,47 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ FileCredStorage.h
+
+Abstract:
+
+ DPAPI-encrypted JSON file credential storage backend.
+
+--*/
+#pragma once
+
+#include "ICredentialStorage.h"
+#include "JsonUtils.h"
+
+namespace wsl::windows::wslc::services {
+
+inline constexpr int CredentialFileVersion = 1;
+
+struct CredentialEntry
+{
+ std::string UserName;
+ std::string Secret;
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialEntry, UserName, Secret);
+};
+
+struct CredentialFile
+{
+ int Version = CredentialFileVersion;
+ std::map Credentials;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialFile, Version, Credentials);
+};
+
+class FileCredStorage final : public ICredentialStorage
+{
+public:
+ void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override;
+ std::pair Get(const std::string& serverAddress) override;
+ void Erase(const std::string& serverAddress) override;
+ std::vector List() override;
+};
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/ICredentialStorage.cpp b/src/windows/wslc/services/ICredentialStorage.cpp
new file mode 100644
index 000000000..6cdd5c2c4
--- /dev/null
+++ b/src/windows/wslc/services/ICredentialStorage.cpp
@@ -0,0 +1,33 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ ICredentialStorage.cpp
+
+Abstract:
+
+ Factory for credential storage backends.
+
+--*/
+
+#include "ICredentialStorage.h"
+#include "FileCredStorage.h"
+#include "WinCredStorage.h"
+#include "WSLCUserSettings.h"
+
+namespace wsl::windows::wslc::services {
+
+std::unique_ptr OpenCredentialStorage()
+{
+ auto backend = settings::User().Get();
+ if (backend == settings::CredentialStoreType::File)
+ {
+ return std::make_unique();
+ }
+
+ return std::make_unique();
+}
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/ICredentialStorage.h b/src/windows/wslc/services/ICredentialStorage.h
new file mode 100644
index 000000000..0ceef96a2
--- /dev/null
+++ b/src/windows/wslc/services/ICredentialStorage.h
@@ -0,0 +1,36 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ ICredentialStorage.h
+
+Abstract:
+
+ Interface for credential storage backends.
+
+--*/
+#pragma once
+
+#include
+#include
+#include
+
+namespace wsl::windows::wslc::services {
+
+// Abstract interface for credential storage backends (WinCred, file-based, etc.).
+struct ICredentialStorage
+{
+ virtual ~ICredentialStorage() = default;
+
+ virtual void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) = 0;
+ virtual std::pair Get(const std::string& serverAddress) = 0;
+ virtual void Erase(const std::string& serverAddress) = 0;
+ virtual std::vector List() = 0;
+};
+
+// Returns the credential storage implementation based on user configuration.
+std::unique_ptr OpenCredentialStorage();
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/PullImageCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp
similarity index 84%
rename from src/windows/wslc/services/PullImageCallback.cpp
rename to src/windows/wslc/services/ImageProgressCallback.cpp
index ebcfd9eb0..412df1096 100644
--- a/src/windows/wslc/services/PullImageCallback.cpp
+++ b/src/windows/wslc/services/ImageProgressCallback.cpp
@@ -4,16 +4,16 @@ Copyright (c) Microsoft. All rights reserved.
Module Name:
- PullImageCallback.cpp
+ ImageProgressCallback.cpp
Abstract:
- This file contains the PullImageCallback Implementation.
+ This file contains the ImageProgressCallback Implementation.
--*/
#include "precomp.h"
-#include "PullImageCallback.h"
+#include "ImageProgressCallback.h"
#include "ImageService.h"
#include
@@ -42,7 +42,7 @@ ChangeTerminalMode::~ChangeTerminalMode()
}
}
-auto PullImageCallback::MoveToLine(SHORT line)
+auto ImageProgressCallback::MoveToLine(SHORT line)
{
if (line > 0)
{
@@ -57,7 +57,7 @@ auto PullImageCallback::MoveToLine(SHORT line)
});
}
-HRESULT PullImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
+HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
{
try
{
@@ -94,14 +94,14 @@ HRESULT PullImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG curren
CATCH_RETURN();
}
-CONSOLE_SCREEN_BUFFER_INFO PullImageCallback::Info()
+CONSOLE_SCREEN_BUFFER_INFO ImageProgressCallback::Info()
{
CONSOLE_SCREEN_BUFFER_INFO info{};
THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info));
return info;
}
-std::wstring PullImageCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info)
+std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info)
{
std::wstring line;
if (total != 0)
diff --git a/src/windows/wslc/services/PullImageCallback.h b/src/windows/wslc/services/ImageProgressCallback.h
similarity index 87%
rename from src/windows/wslc/services/PullImageCallback.h
rename to src/windows/wslc/services/ImageProgressCallback.h
index b25ce0717..4a9f7ec74 100644
--- a/src/windows/wslc/services/PullImageCallback.h
+++ b/src/windows/wslc/services/ImageProgressCallback.h
@@ -4,11 +4,11 @@ Copyright (c) Microsoft. All rights reserved.
Module Name:
- PullImageCallback.h
+ ImageProgressCallback.h
Abstract:
- This file contains the PullImageCallback definition
+ This file contains the ImageProgressCallback definition
--*/
#pragma once
@@ -35,7 +35,7 @@ class ChangeTerminalMode
};
// TODO: Handle terminal resizes.
-class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") PullImageCallback
+class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallback
: public Microsoft::WRL::RuntimeClass, IProgressCallback, IFastRundown>
{
public:
diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp
index 34d4aadd6..0910c4f0a 100644
--- a/src/windows/wslc/services/ImageService.cpp
+++ b/src/windows/wslc/services/ImageService.cpp
@@ -12,6 +12,7 @@ Module Name:
--*/
#include "ImageService.h"
+#include "RegistryService.h"
#include "SessionService.h"
#include
#include
@@ -63,6 +64,13 @@ wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath)
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBuildFileNotFound(contextPath));
}
+std::string GetServerFromImage(const std::string& image)
+{
+ auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(image);
+ auto [server, path] = wsl::windows::common::wslutil::NormalizeRepo(repo);
+ return server;
+}
+
} // namespace
namespace wsl::windows::wslc::services {
@@ -196,7 +204,9 @@ void ImageService::Delete(wsl::windows::wslc::models::Session& session, const st
void ImageService::Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
{
- THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), nullptr, callback));
+ auto server = GetServerFromImage(image);
+ auto auth = RegistryService::Get(server);
+ THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth.c_str(), callback));
}
void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage)
@@ -223,8 +233,11 @@ InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session,
return wsl::shared::FromJson(inspectData.get());
}
-void ImageService::Push()
+void ImageService::Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
{
+ auto server = GetServerFromImage(image);
+ auto auth = RegistryService::Get(server);
+ THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback));
}
void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent)
diff --git a/src/windows/wslc/services/ImageService.h b/src/windows/wslc/services/ImageService.h
index 4d97b55e2..dfaac435c 100644
--- a/src/windows/wslc/services/ImageService.h
+++ b/src/windows/wslc/services/ImageService.h
@@ -36,9 +36,9 @@ class ImageService
static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune);
static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image);
static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
+ static void Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback);
static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent = nullptr);
static void Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage);
- void Push();
void Prune();
};
} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp
new file mode 100644
index 000000000..5e227d29b
--- /dev/null
+++ b/src/windows/wslc/services/RegistryService.cpp
@@ -0,0 +1,105 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryService.cpp
+
+Abstract:
+
+ This file contains the RegistryService implementation
+
+--*/
+
+#include "RegistryService.h"
+#include
+
+using namespace wsl::windows::common::wslutil;
+
+namespace {
+
+std::string ResolveCredentialKey(const std::string& serverAddress)
+{
+ auto input = serverAddress;
+
+ // Strip scheme
+ if (auto pos = input.find("://"); pos != std::string::npos)
+ {
+ input = input.substr(pos + 3);
+ }
+
+ // Strip path
+ if (auto pos = input.find('/'); pos != std::string::npos)
+ {
+ input = input.substr(0, pos);
+ }
+
+ // Map Docker Hub aliases to canonical key.
+ if (input == "docker.io" || input == "index.docker.io")
+ {
+ return wsl::windows::wslc::services::RegistryService::DefaultServer;
+ }
+
+ return input;
+}
+} // namespace
+
+namespace wsl::windows::wslc::services {
+
+// Sentinel username matching Docker's convention for identity-token credentials.
+static constexpr auto TokenUsername = "";
+
+void RegistryService::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
+{
+ THROW_HR_IF(E_INVALIDARG, serverAddress.empty());
+ THROW_HR_IF(E_INVALIDARG, secret.empty());
+
+ auto storage = OpenCredentialStorage();
+ storage->Store(ResolveCredentialKey(serverAddress), username, secret);
+}
+
+std::string RegistryService::Get(const std::string& serverAddress)
+{
+ auto storage = OpenCredentialStorage();
+ auto key = ResolveCredentialKey(serverAddress);
+ auto [username, secret] = storage->Get(key);
+
+ if (username == TokenUsername)
+ {
+ return BuildRegistryAuthHeader(secret);
+ }
+
+ return BuildRegistryAuthHeader(username, secret);
+}
+
+void RegistryService::Erase(const std::string& serverAddress)
+{
+ THROW_HR_IF(E_INVALIDARG, serverAddress.empty());
+
+ auto storage = OpenCredentialStorage();
+ storage->Erase(ResolveCredentialKey(serverAddress));
+}
+
+std::vector RegistryService::List()
+{
+ auto storage = OpenCredentialStorage();
+ return storage->List();
+}
+
+std::pair RegistryService::Authenticate(
+ wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password)
+{
+ wil::unique_cotaskmem_ansistring identityToken;
+ THROW_IF_FAILED(session.Get()->Authenticate(serverAddress.c_str(), username.c_str(), password.c_str(), &identityToken));
+
+ // If the registry returned an identity token, use it. Otherwise fall back to username/password.
+ if (identityToken && strlen(identityToken.get()) > 0)
+ {
+ return {TokenUsername, identityToken.get()};
+ }
+
+ return {username, password};
+}
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h
new file mode 100644
index 000000000..5494c29ec
--- /dev/null
+++ b/src/windows/wslc/services/RegistryService.h
@@ -0,0 +1,37 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryService.h
+
+Abstract:
+
+ This file contains the RegistryService definition
+
+--*/
+#pragma once
+
+#include "ICredentialStorage.h"
+#include "SessionModel.h"
+
+namespace wsl::windows::wslc::services {
+
+// High-level registry authentication service.
+// Delegates credential persistence to ICredentialStorage (selected via OpenCredentialStorage).
+class RegistryService
+{
+public:
+ static void Store(const std::string& serverAddress, const std::string& username, const std::string& secret);
+ static std::string Get(const std::string& serverAddress);
+ static void Erase(const std::string& serverAddress);
+ static std::vector List();
+ static std::pair Authenticate(
+ wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password);
+
+ // Default registry server address used when no explicit server is provided.
+ static constexpr auto DefaultServer = "https://index.docker.io/v1/";
+};
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/WinCredStorage.cpp b/src/windows/wslc/services/WinCredStorage.cpp
new file mode 100644
index 000000000..2f4bdf616
--- /dev/null
+++ b/src/windows/wslc/services/WinCredStorage.cpp
@@ -0,0 +1,113 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WinCredStorage.cpp
+
+Abstract:
+
+ Windows Credential Manager credential storage implementation.
+
+--*/
+
+#include "precomp.h"
+#include "WinCredStorage.h"
+#include
+
+using wsl::shared::Localization;
+
+using unique_credential = wil::unique_any;
+using unique_credential_array = wil::unique_any;
+
+static constexpr auto WinCredPrefix = L"wslc-credential/";
+
+namespace wsl::windows::wslc::services {
+
+std::wstring WinCredStorage::TargetName(const std::string& serverAddress)
+{
+ return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress);
+}
+
+void WinCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
+{
+ auto targetName = TargetName(serverAddress);
+ auto wideUsername = wsl::shared::string::MultiByteToWide(username);
+
+ CREDENTIALW cred{};
+ cred.Type = CRED_TYPE_GENERIC;
+ cred.TargetName = const_cast(targetName.c_str());
+ cred.UserName = const_cast(wideUsername.c_str());
+ cred.CredentialBlobSize = static_cast(secret.size());
+ cred.CredentialBlob = reinterpret_cast(const_cast(secret.data()));
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+
+ THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0));
+}
+
+std::pair WinCredStorage::Get(const std::string& serverAddress)
+{
+ auto targetName = TargetName(serverAddress);
+
+ unique_credential cred;
+ if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred))
+ {
+ THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
+ return {};
+ }
+
+ if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr)
+ {
+ return {};
+ }
+
+ std::string username;
+ if (cred.get()->UserName)
+ {
+ username = wsl::shared::string::WideToMultiByte(cred.get()->UserName);
+ }
+
+ return {std::move(username), {reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize}};
+}
+
+void WinCredStorage::Erase(const std::string& serverAddress)
+{
+ auto targetName = TargetName(serverAddress);
+
+ if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0))
+ {
+ auto error = GetLastError();
+ THROW_HR_WITH_USER_ERROR_IF(
+ E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), error == ERROR_NOT_FOUND);
+
+ THROW_WIN32(error);
+ }
+}
+
+std::vector WinCredStorage::List()
+{
+ auto prefix = std::wstring(WinCredPrefix);
+ auto filter = prefix + L"*";
+
+ DWORD count = 0;
+ unique_credential_array creds;
+ if (!CredEnumerateW(filter.c_str(), 0, &count, &creds))
+ {
+ THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND);
+ return {};
+ }
+
+ std::vector result;
+ result.reserve(count);
+
+ for (DWORD i = 0; i < count; ++i)
+ {
+ std::wstring_view name(creds.get()[i]->TargetName);
+ result.emplace_back(name.substr(prefix.size()));
+ }
+
+ return result;
+}
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/WinCredStorage.h b/src/windows/wslc/services/WinCredStorage.h
new file mode 100644
index 000000000..35dd2845e
--- /dev/null
+++ b/src/windows/wslc/services/WinCredStorage.h
@@ -0,0 +1,32 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WinCredStorage.h
+
+Abstract:
+
+ Windows Credential Manager credential storage backend.
+
+--*/
+#pragma once
+
+#include "ICredentialStorage.h"
+
+namespace wsl::windows::wslc::services {
+
+class WinCredStorage final : public ICredentialStorage
+{
+public:
+ void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override;
+ std::pair Get(const std::string& serverAddress) override;
+ void Erase(const std::string& serverAddress) override;
+ std::vector List() override;
+
+private:
+ static std::wstring TargetName(const std::string& serverAddress);
+};
+
+} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp
index 461e00589..058708af4 100644
--- a/src/windows/wslc/tasks/ImageTasks.cpp
+++ b/src/windows/wslc/tasks/ImageTasks.cpp
@@ -19,7 +19,7 @@ Module Name:
#include "ImageModel.h"
#include "ImageService.h"
#include "ImageTasks.h"
-#include "PullImageCallback.h"
+#include "ImageProgressCallback.h"
#include "TableOutput.h"
#include "Task.h"
#include
@@ -136,10 +136,21 @@ void PullImage(CLIExecutionContext& context)
auto& session = context.Data.Get();
auto& imageId = context.Args.Get();
- PullImageCallback callback;
+ ImageProgressCallback callback;
services::ImageService::Pull(session, WideToMultiByte(imageId), &callback);
}
+void PushImage(CLIExecutionContext& context)
+{
+ WI_ASSERT(context.Data.Contains(Data::Session));
+ WI_ASSERT(context.Args.Contains(ArgType::ImageId));
+ auto& session = context.Data.Get();
+ auto& imageId = context.Args.Get();
+
+ ImageProgressCallback callback;
+ services::ImageService::Push(session, WideToMultiByte(imageId), &callback);
+}
+
void DeleteImage(CLIExecutionContext& context)
{
WI_ASSERT(context.Data.Contains(Data::Session));
diff --git a/src/windows/wslc/tasks/ImageTasks.h b/src/windows/wslc/tasks/ImageTasks.h
index ae4f61e70..745168421 100644
--- a/src/windows/wslc/tasks/ImageTasks.h
+++ b/src/windows/wslc/tasks/ImageTasks.h
@@ -22,6 +22,7 @@ void GetImages(CLIExecutionContext& context);
void ListImages(CLIExecutionContext& context);
void LoadImage(CLIExecutionContext& context);
void PullImage(CLIExecutionContext& context);
+void PushImage(CLIExecutionContext& context);
void DeleteImage(CLIExecutionContext& context);
void InspectImages(CLIExecutionContext& context);
void TagImage(CLIExecutionContext& context);
diff --git a/src/windows/wslc/tasks/RegistryTasks.cpp b/src/windows/wslc/tasks/RegistryTasks.cpp
new file mode 100644
index 000000000..279a1ead4
--- /dev/null
+++ b/src/windows/wslc/tasks/RegistryTasks.cpp
@@ -0,0 +1,66 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryTasks.cpp
+
+Abstract:
+
+ Implementation of registry command related execution logic.
+
+--*/
+#include "Argument.h"
+#include "CLIExecutionContext.h"
+#include "RegistryService.h"
+#include "RegistryTasks.h"
+#include "Task.h"
+
+using namespace wsl::shared;
+using namespace wsl::windows::common::string;
+using namespace wsl::windows::common::wslutil;
+using namespace wsl::windows::wslc::execution;
+using namespace wsl::windows::wslc::services;
+
+namespace wsl::windows::wslc::task {
+
+void Login(CLIExecutionContext& context)
+{
+ WI_ASSERT(context.Data.Contains(Data::Session));
+ WI_ASSERT(context.Args.Contains(ArgType::Username));
+ WI_ASSERT(context.Args.Contains(ArgType::Password));
+
+ auto& session = context.Data.Get();
+
+ auto username = WideToMultiByte(context.Args.Get());
+ auto password = WideToMultiByte(context.Args.Get());
+
+ auto serverAddress = std::string(RegistryService::DefaultServer);
+
+ if (context.Args.Contains(ArgType::Server))
+ {
+ serverAddress = WideToMultiByte(context.Args.Get());
+ }
+
+ auto [credUsername, credSecret] = RegistryService::Authenticate(session, serverAddress, username, password);
+ RegistryService::Store(serverAddress, credUsername, credSecret);
+
+ PrintMessage(Localization::WSLCCLI_LoginSucceeded());
+}
+
+void Logout(CLIExecutionContext& context)
+{
+ auto serverAddress = std::string(RegistryService::DefaultServer);
+
+ if (context.Args.Contains(ArgType::Server))
+ {
+ serverAddress = WideToMultiByte(context.Args.Get());
+ }
+
+ RegistryService::Erase(serverAddress);
+
+ PrintMessage(Localization::WSLCCLI_LogoutSucceeded(MultiByteToWide(serverAddress)));
+}
+
+} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslc/tasks/RegistryTasks.h b/src/windows/wslc/tasks/RegistryTasks.h
new file mode 100644
index 000000000..59477ce27
--- /dev/null
+++ b/src/windows/wslc/tasks/RegistryTasks.h
@@ -0,0 +1,22 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ RegistryTasks.h
+
+Abstract:
+
+ Declaration of registry command execution tasks.
+
+--*/
+#pragma once
+#include "CLIExecutionContext.h"
+
+using wsl::windows::wslc::execution::CLIExecutionContext;
+
+namespace wsl::windows::wslc::task {
+void Login(CLIExecutionContext& context);
+void Logout(CLIExecutionContext& context);
+} // namespace wsl::windows::wslc::task
diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp
index 948a5f826..00f3467bf 100644
--- a/test/windows/Common.cpp
+++ b/test/windows/Common.cpp
@@ -68,6 +68,7 @@ static std::wstring g_pipelineBuildId;
std::wstring g_testDistroPath;
std::wstring g_testDataPath;
bool g_fastTestRun = false; // True when test.bat was invoked with -f
+static wil::unique_mta_usage_cookie g_mtaCookie;
std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa)
{
@@ -1974,6 +1975,8 @@ Return Value:
{
wsl::windows::common::wslutil::InitializeWil();
+ THROW_IF_FAILED(CoIncrementMTAUsage(&g_mtaCookie));
+
// Don't crash for unknown exceptions (makes debugging testpasses harder)
#ifndef _DEBUG
wil::g_fResultFailFastUnknownExceptions = false;
@@ -2188,6 +2191,7 @@ Return Value:
}
WslTraceLoggingUninitialize();
+ g_mtaCookie.reset();
return true;
}
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index 2f54dc09c..606ead20c 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -36,7 +36,6 @@ class WSLCTests
{
WSLC_TEST_CLASS(WSLCTests)
- wil::unique_mta_usage_cookie m_mtaCookie;
WSADATA m_wsadata;
std::filesystem::path m_storagePath;
WSLCSessionSettings m_defaultSessionSettings{};
@@ -59,7 +58,6 @@ class WSLCTests
TEST_CLASS_SETUP(TestClassSetup)
{
- THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie));
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
// The WSLC SDK tests use this same storage to reduce pull overhead.
@@ -482,7 +480,7 @@ class WSLCTests
// Start a local registry without auth and push hello-world:latest to it.
auto [registryContainer, registryAddress] = StartLocalRegistry();
- auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", "", registryAddress));
+ auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", ""));
ExpectImagePresent(*m_defaultSession, image.c_str(), false);
VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr));
@@ -524,7 +522,7 @@ class WSLCTests
{
// Start a local registry without auth to avoid Docker Hub rate limits.
auto [registryContainer, registryAddress] = StartLocalRegistry();
- auto auth = BuildRegistryAuthHeader("", "", registryAddress);
+ auto auth = BuildRegistryAuthHeader("", "");
auto validatePull = [&](const std::string& sourceImage) {
// Push the source image to the local registry.
@@ -606,7 +604,7 @@ class WSLCTests
WSLC_TEST_METHOD(PushImage)
{
- auto emptyAuth = BuildRegistryAuthHeader("", "", "");
+ auto emptyAuth = BuildRegistryAuthHeader("", "");
// Validate that pushing a non-existent image fails.
{
@@ -642,7 +640,7 @@ class WSLCTests
VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token));
VERIFY_IS_NOT_NULL(token.get());
- auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password, registryAddress);
+ auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password);
auto image = PushImageToRegistry("hello-world:latest", registryAddress, xRegistryAuth);
// Pulling without credentials should fail.
diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp
index cc23d5c0a..8560d0ac3 100644
--- a/test/windows/WslcSdkTests.cpp
+++ b/test/windows/WslcSdkTests.cpp
@@ -168,7 +168,6 @@ class WslcSdkTests
{
WSLC_TEST_CLASS(WslcSdkTests)
- wil::unique_mta_usage_cookie m_mtaCookie;
WSADATA m_wsadata;
std::filesystem::path m_storagePath;
WslcSession m_defaultSession = nullptr;
@@ -182,7 +181,6 @@ class WslcSdkTests
TEST_CLASS_SETUP(TestClassSetup)
{
- THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie));
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
// Use the same storage path as WSLC runtime tests to reduce pull overhead.
@@ -2113,7 +2111,7 @@ class WslcSdkTests
VERIFY_IS_NOT_NULL(token.get());
}
- auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password, registryAddress);
+ auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password);
PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
auto image = std::format("{}/hello-world:latest", registryAddress);
@@ -2139,7 +2137,7 @@ class WslcSdkTests
// Negative: Pulling with bad credentials should fail.
{
- auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong", registryAddress);
+ auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong");
WslcPullImageOptions opts{};
opts.uri = image.c_str();
@@ -2164,7 +2162,7 @@ class WslcSdkTests
{
// Start a local registry without auth to avoid Docker Hub rate limits.
auto [registryContainer, registryAddress] = StartLocalRegistry();
- auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", registryAddress);
+ auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
{
// Push hello-world:latest to the local registry.
@@ -2214,7 +2212,7 @@ class WslcSdkTests
WSLC_TEST_METHOD(PushImage)
{
- auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", "");
+ auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
// Negative: pushing a non-existent image must fail.
{
diff --git a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp
new file mode 100644
index 000000000..2c38965f3
--- /dev/null
+++ b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp
@@ -0,0 +1,163 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCCredStorageUnitTests.cpp
+
+Abstract:
+
+ Unit tests for the FileCredStorage and WinCredStorage credential
+ storage backends. Tests Store, Get, List, and Erase operations.
+
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "FileCredStorage.h"
+#include "WinCredStorage.h"
+
+using namespace wsl::windows::wslc::services;
+using namespace WEX::Logging;
+using namespace WEX::Common;
+using namespace WEX::TestExecution;
+
+namespace WSLCCredStorageUnitTests {
+
+class WSLCCLICredStorageUnitTests
+{
+ WSL_TEST_CLASS(WSLCCLICredStorageUnitTests)
+
+ FileCredStorage m_fileStorage;
+ WinCredStorage m_winCredStorage;
+
+ static void TestStoreAndGetRoundTrips(ICredentialStorage& storage)
+ {
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { storage.Erase("wslc-test-server1"); });
+ storage.Store("wslc-test-server1", "test-user", "credential-data-1");
+
+ auto [username, secret] = storage.Get("wslc-test-server1");
+ VERIFY_ARE_EQUAL(std::string("test-user"), username);
+ VERIFY_ARE_EQUAL(std::string("credential-data-1"), secret);
+ }
+
+ WSLC_TEST_METHOD(FileCred_Store_And_Get_RoundTrips)
+ {
+ TestStoreAndGetRoundTrips(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_Store_And_Get_RoundTrips)
+ {
+ TestStoreAndGetRoundTrips(m_winCredStorage);
+ }
+
+ static void TestGetNonExistentReturnsEmpty(ICredentialStorage& storage)
+ {
+ auto [username, secret] = storage.Get("wslc-test-nonexistent-server");
+ VERIFY_IS_TRUE(username.empty());
+ VERIFY_IS_TRUE(secret.empty());
+ }
+
+ WSLC_TEST_METHOD(FileCred_Get_NonExistent_ReturnsEmpty)
+ {
+ TestGetNonExistentReturnsEmpty(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_Get_NonExistent_ReturnsEmpty)
+ {
+ TestGetNonExistentReturnsEmpty(m_winCredStorage);
+ }
+
+ static void TestStoreOverwritesExistingCredential(ICredentialStorage& storage)
+ {
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { storage.Erase("wslc-test-server2"); });
+ storage.Store("wslc-test-server2", "old-user", "old-credential");
+ storage.Store("wslc-test-server2", "new-user", "new-credential");
+
+ auto [username, secret] = storage.Get("wslc-test-server2");
+ VERIFY_ARE_EQUAL(std::string("new-user"), username);
+ VERIFY_ARE_EQUAL(std::string("new-credential"), secret);
+ }
+
+ WSLC_TEST_METHOD(FileCred_Store_Overwrites_ExistingCredential)
+ {
+ TestStoreOverwritesExistingCredential(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_Store_Overwrites_ExistingCredential)
+ {
+ TestStoreOverwritesExistingCredential(m_winCredStorage);
+ }
+
+ static void TestListContainsStoredServers(ICredentialStorage& storage)
+ {
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
+ storage.Erase("wslc-test-list1");
+ storage.Erase("wslc-test-list2");
+ });
+ storage.Store("wslc-test-list1", "user1", "cred1");
+ storage.Store("wslc-test-list2", "user2", "cred2");
+
+ auto servers = storage.List();
+ bool found1 = false, found2 = false;
+ for (const auto& s : servers)
+ {
+ if (s == L"wslc-test-list1")
+ {
+ found1 = true;
+ }
+ if (s == L"wslc-test-list2")
+ {
+ found2 = true;
+ }
+ }
+
+ VERIFY_IS_TRUE(found1);
+ VERIFY_IS_TRUE(found2);
+ }
+
+ WSLC_TEST_METHOD(FileCred_List_ContainsStoredServers)
+ {
+ TestListContainsStoredServers(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_List_ContainsStoredServers)
+ {
+ TestListContainsStoredServers(m_winCredStorage);
+ }
+
+ static void TestEraseRemovesCredential(ICredentialStorage& storage)
+ {
+ storage.Store("wslc-test-erase", "user", "cred");
+ auto [username, secret] = storage.Get("wslc-test-erase");
+ VERIFY_IS_FALSE(username.empty());
+
+ storage.Erase("wslc-test-erase");
+ auto [username2, secret2] = storage.Get("wslc-test-erase");
+ VERIFY_IS_TRUE(username2.empty());
+ }
+
+ WSLC_TEST_METHOD(FileCred_Erase_RemovesCredential)
+ {
+ TestEraseRemovesCredential(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_Erase_RemovesCredential)
+ {
+ TestEraseRemovesCredential(m_winCredStorage);
+ }
+
+ static void TestEraseNonExistentThrows(ICredentialStorage& storage)
+ {
+ VERIFY_THROWS_SPECIFIC(storage.Erase("wslc-test-nonexistent-erase"), wil::ResultException, [](const wil::ResultException& e) {
+ return e.GetErrorCode() == E_NOT_SET;
+ });
+ }
+
+ WSLC_TEST_METHOD(FileCred_Erase_NonExistent_Throws)
+ {
+ TestEraseNonExistentThrows(m_fileStorage);
+ }
+ WSLC_TEST_METHOD(WinCred_Erase_NonExistent_Throws)
+ {
+ TestEraseNonExistentThrows(m_winCredStorage);
+ }
+};
+
+} // namespace WSLCCredStorageUnitTests
diff --git a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp
index e7152e89a..e2dcc7a23 100644
--- a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp
@@ -95,6 +95,7 @@ class WSLCCLISettingsUnitTests
VERIFY_ARE_EQUAL(2048u, map.GetOrDefault());
VERIFY_ARE_EQUAL(102400u, map.GetOrDefault());
VERIFY_ARE_EQUAL(std::wstring{}, map.GetOrDefault());
+ VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(map.GetOrDefault()));
}
// After inserting a value, GetOrDefault must return it rather than the default.
@@ -122,6 +123,7 @@ class WSLCCLISettingsUnitTests
VERIFY_ARE_EQUAL(2048u, s.Get());
VERIFY_ARE_EQUAL(102400u, s.Get());
VERIFY_ARE_EQUAL(std::wstring{}, s.Get());
+ VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(s.Get()));
}
// -----------------------------------------------------------------------
@@ -138,7 +140,8 @@ class WSLCCLISettingsUnitTests
"session:\n"
" cpuCount: 8\n"
" memorySize: 4GB\n"
- " maxStorageSize: 20000MB\n");
+ " maxStorageSize: 20000MB\n"
+ "credentialStore: file\n");
UserSettingsTest s{dir};
@@ -149,6 +152,7 @@ class WSLCCLISettingsUnitTests
VERIFY_ARE_EQUAL(20000u, s.Get());
// Unspecified setting falls back to built-in default.
VERIFY_ARE_EQUAL(std::wstring{}, s.Get());
+ VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::File), static_cast(s.Get()));
}
// An empty settings file is valid YAML (null document); all settings use
@@ -272,6 +276,18 @@ class WSLCCLISettingsUnitTests
VERIFY_ARE_EQUAL(std::wstring{}, s.Get());
}
+ // credentialStore: invalid value must fall back to default and warn.
+ TEST_METHOD(Validation_CredentialStore_Invalid_UsesDefaultAndWarns)
+ {
+ auto dir = UniqueTempDir();
+ WriteFile(dir / L"settings.yaml", "credentialStore: badvalue\n");
+
+ UserSettingsTest s{dir};
+
+ VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(s.Get()));
+ VERIFY_IS_TRUE(s.GetWarnings().size() >= 1u);
+ }
+
// Extra unknown keys at any level must not cause errors or warnings.
TEST_METHOD(Validation_UnknownKeys_NoErrorsOrWarnings)
{
diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
index 81c353471..94fa323f7 100644
--- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
@@ -525,6 +525,7 @@ class WSLCE2EGlobalTests
std::vector> entries = {
{L"container", Localization::WSLCCLI_ContainerCommandDesc()},
{L"image", Localization::WSLCCLI_ImageCommandDesc()},
+ {L"registry", Localization::WSLCCLI_RegistryCommandDesc()},
{L"session", Localization::WSLCCLI_SessionCommandDesc()},
{L"settings", Localization::WSLCCLI_SettingsCommandDesc()},
{L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
@@ -536,8 +537,11 @@ class WSLCE2EGlobalTests
{L"kill", Localization::WSLCCLI_ContainerKillDesc()},
{L"list", Localization::WSLCCLI_ContainerListDesc()},
{L"load", Localization::WSLCCLI_ImageLoadDesc()},
+ {L"login", Localization::WSLCCLI_LoginDesc()},
+ {L"logout", Localization::WSLCCLI_LogoutDesc()},
{L"logs", Localization::WSLCCLI_ContainerLogsDesc()},
{L"pull", Localization::WSLCCLI_ImagePullDesc()},
+ {L"push", Localization::WSLCCLI_ImagePushDesc()},
{L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
{L"rmi", Localization::WSLCCLI_ImageRemoveDesc()},
{L"run", Localization::WSLCCLI_ContainerRunDesc()},
diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
index 4448ce665..2071cedab 100644
--- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp
@@ -18,6 +18,7 @@ Module Name:
#include "WSLCExecutor.h"
#include "WSLCE2EHelpers.h"
#include
+#include
extern std::wstring g_testDataPath;
@@ -367,6 +368,56 @@ void EnsureSessionIsTerminated(const std::wstring& sessionName)
}
}
+wil::com_ptr OpenDefaultElevatedSession()
+{
+ // Ensure the default elevated session exists before opening it via COM.
+ RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0});
+
+ wil::com_ptr sessionManager;
+ VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
+ wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
+
+ wil::com_ptr session;
+ VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(nullptr, &session));
+ wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
+
+ return std::move(session);
+}
+
+std::pair StartLocalRegistry(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port)
+{
+ EnsureImageIsLoaded({L"wslc-registry", L"latest", GetTestImagePath("wslc-registry:latest")});
+
+ std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
+
+ if (!username.empty())
+ {
+ env.push_back(std::format("USERNAME={}", username));
+ env.push_back(std::format("PASSWORD={}", password));
+ }
+
+ WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
+ launcher.SetEntrypoint({"/entrypoint.sh"});
+ launcher.AddPort(port, port, AF_INET);
+
+ auto container = launcher.Launch(session, WSLCContainerStartFlagsNone);
+
+ auto address = std::format("127.0.0.1:{}", port);
+ auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address));
+
+ int expectedCode = username.empty() ? 200 : 401;
+ ExpectHttpResponse(url.c_str(), expectedCode, true);
+
+ return {std::move(container), std::move(address)};
+}
+
+std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress)
+{
+ auto registryImage = std::format(L"{}/{}", registryAddress, imageName);
+ RunWslcAndVerify(std::format(L"image tag {} {}", imageName, registryImage), {.ExitCode = 0});
+ return registryImage;
+}
+
void WriteTestFile(const std::filesystem::path& filePath, const std::vector& lines)
{
std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h
index a09bdefec..1042d07e3 100644
--- a/test/windows/wslc/e2e/WSLCE2EHelpers.h
+++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h
@@ -18,6 +18,7 @@ Module Name:
#include
#include
#include
+#include
namespace WSLCE2ETests {
@@ -180,4 +181,15 @@ inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId)
{
VerifyContainerIsNotListed(containerNameOrId, std::chrono::milliseconds(0), std::chrono::milliseconds(0));
}
-} // namespace WSLCE2ETests
\ No newline at end of file
+
+wil::com_ptr OpenDefaultElevatedSession();
+
+// Starts a local registry container with host networking using the COM API.
+// Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT").
+std::pair StartLocalRegistry(
+ IWSLCSession& session, const std::string& username = "", const std::string& password = "", USHORT port = 5000);
+
+// Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest").
+std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress);
+
+} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp
index 8a2142804..3b20d656b 100644
--- a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp
+++ b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp
@@ -73,6 +73,7 @@ class WSLCE2EImageTests
{L"list", Localization::WSLCCLI_ImageListDesc()},
{L"load", Localization::WSLCCLI_ImageLoadDesc()},
{L"pull", Localization::WSLCCLI_ImagePullDesc()},
+ {L"push", Localization::WSLCCLI_ImagePushDesc()},
{L"save", Localization::WSLCCLI_ImageSaveDesc()},
{L"tag", Localization::WSLCCLI_ImageTagDesc()},
};
diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp
new file mode 100644
index 000000000..28d01825b
--- /dev/null
+++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp
@@ -0,0 +1,181 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCE2EPushPullTests.cpp
+
+Abstract:
+
+ End-to-end tests for wslc image push and pull against a local registry.
+
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCExecutor.h"
+#include "WSLCE2EHelpers.h"
+#include "Argument.h"
+
+namespace WSLCE2ETests {
+using namespace wsl::shared;
+
+class WSLCE2EPushPullTests
+{
+ WSLC_TEST_CLASS(WSLCE2EPushPullTests)
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand)
+ {
+ auto result = RunWslc(L"image push --help");
+ result.Verify({.Stdout = GetPushHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Push_RootAlias)
+ {
+ auto result = RunWslc(L"push --help");
+ result.Verify({.Stdout = GetPushRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Pull_HelpCommand)
+ {
+ auto result = RunWslc(L"image pull --help");
+ result.Verify({.Stdout = GetPullHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Pull_RootAlias)
+ {
+ auto result = RunWslc(L"pull --help");
+ result.Verify({.Stdout = GetPullRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_PushPull)
+ {
+ const auto& debianImage = DebianTestImage();
+ EnsureImageIsLoaded(debianImage);
+
+ // Start a local registry without auth.
+ auto session = OpenDefaultElevatedSession();
+
+ {
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 15003);
+ auto registryAddressW = string::MultiByteToWide(registryAddress);
+
+ // Tag the image for the local registry.
+ auto registryImage = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW);
+
+ auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImage)); });
+
+ // Push should succeed.
+ auto result = RunWslc(std::format(L"push {}", registryImage));
+ result.Verify({.ExitCode = 0});
+
+ // Delete the local copy and pull it back.
+ RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
+
+ result = RunWslc(std::format(L"pull {}", registryImage));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Verify the image is now present.
+ result = RunWslc(L"image list -q");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+ VERIFY_IS_TRUE(result.Stdout.has_value());
+ VERIFY_IS_TRUE(result.Stdout->find(registryImage) != std::wstring::npos);
+ }
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Push_NonExistentImage)
+ {
+ auto result = RunWslc(L"push does-not-exist:latest");
+ auto errorMessage = L"An image does not exist locally with the tag: does-not-exist\r\nError code: E_FAIL\r\n";
+ result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Image_Pull_NonExistentImage)
+ {
+ auto result = RunWslc(L"pull does-not-exist:latest");
+ auto errorMessage =
+ L"pull access denied for does-not-exist, repository does not exist or may require 'docker login': denied: requested "
+ L"access to the resource is denied\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n";
+ result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
+ }
+
+private:
+ std::wstring GetPushHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetPushDescription() << GetPushUsage() << GetAvailableArguments() << GetAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetPushRootAliasHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetPushDescription() << GetPushRootUsage() << GetAvailableArguments() << GetAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetPullHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetPullDescription() << GetPullUsage() << GetAvailableArguments() << GetAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetPullRootAliasHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetPullDescription() << GetPullRootUsage() << GetAvailableArguments() << GetAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetPushDescription() const
+ {
+ return Localization::WSLCCLI_ImagePushLongDesc() + L"\r\n\r\n";
+ }
+
+ std::wstring GetPullDescription() const
+ {
+ return Localization::WSLCCLI_ImagePullLongDesc() + L"\r\n\r\n";
+ }
+
+ std::wstring GetPushUsage() const
+ {
+ return L"Usage: wslc image push [] \r\n\r\n";
+ }
+
+ std::wstring GetPushRootUsage() const
+ {
+ return L"Usage: wslc push [] \r\n\r\n";
+ }
+
+ std::wstring GetPullUsage() const
+ {
+ return L"Usage: wslc image pull [] \r\n\r\n";
+ }
+
+ std::wstring GetPullRootUsage() const
+ {
+ return L"Usage: wslc pull [] \r\n\r\n";
+ }
+
+ std::wstring GetAvailableArguments() const
+ {
+ std::wstringstream args;
+ args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
+ << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n"
+ << L"\r\n";
+ return args.str();
+ }
+
+ std::wstring GetAvailableOptions() const
+ {
+ std::wstringstream options;
+ options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
+ << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
+ << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
+ << L"\r\n";
+ return options.str();
+ }
+};
+} // namespace WSLCE2ETests
diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp
new file mode 100644
index 000000000..14c1b7a5d
--- /dev/null
+++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp
@@ -0,0 +1,292 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCE2ERegistryTests.cpp
+
+Abstract:
+
+ End-to-end tests for wslc registry login/logout auth flows against a local registry.
+
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCExecutor.h"
+#include "WSLCE2EHelpers.h"
+#include "Argument.h"
+#include
+
+namespace WSLCE2ETests {
+using namespace wsl::shared;
+using namespace WEX::Logging;
+
+namespace {
+
+ constexpr auto c_username = "wslctest";
+ constexpr auto c_password = "password";
+
+ void VerifyAuthFailure(const WSLCExecutionResult& result)
+ {
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"no basic auth credentials") != std::wstring::npos);
+ }
+
+ void VerifyLogoutSucceeds(const std::wstring& registryAddress)
+ {
+ auto result = RunWslc(std::format(L"logout {}", registryAddress));
+ result.Verify({.Stdout = Localization::WSLCCLI_LogoutSucceeded(registryAddress) + L"\r\n", .Stderr = L"", .ExitCode = 0});
+ }
+} // namespace
+
+class WSLCE2ERegistryTests
+{
+ WSLC_TEST_CLASS(WSLCE2ERegistryTests)
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_LoginLogout_PushPull_AuthFlow)
+ {
+ const auto& debianImage = DebianTestImage();
+ EnsureImageIsLoaded(debianImage);
+
+ auto session = OpenDefaultElevatedSession();
+
+ {
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001);
+ auto registryAddressW = string::MultiByteToWide(registryAddress);
+
+ auto registryImageName = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW);
+
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
+ RunWslc(std::format(L"image delete --force {}", registryImageName));
+ RunWslc(std::format(L"logout {}", registryAddressW));
+ });
+
+ // Negative path before login: push and pull should fail.
+ auto result = RunWslc(std::format(L"push {}", registryImageName));
+ VerifyAuthFailure(result);
+
+ RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
+
+ result = RunWslc(std::format(L"pull {}", registryImageName));
+ VerifyAuthFailure(result);
+
+ // Login and verify that saved credentials are used for push/pull.
+ result = RunWslc(std::format(
+ L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW));
+ result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
+
+ registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW);
+ result = RunWslc(std::format(L"push {}", registryImageName));
+ result.Verify({.ExitCode = 0});
+
+ RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
+ result = RunWslc(std::format(L"pull {}", registryImageName));
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Logout and verify both pull and push fail again.
+ VerifyLogoutSucceeds(registryAddressW);
+
+ RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0});
+ result = RunWslc(std::format(L"pull {}", registryImageName));
+ VerifyAuthFailure(result);
+
+ registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW);
+ result = RunWslc(std::format(L"push {}", registryImageName));
+ VerifyAuthFailure(result);
+
+ // Negative path for logout command: second logout should fail.
+ result = RunWslc(std::format(L"logout {}", registryAddressW));
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"Not logged in to") != std::wstring::npos);
+ }
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Login_HelpCommand)
+ {
+ auto result = RunWslc(L"registry login --help");
+ result.Verify({.Stdout = GetLoginHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Logout_HelpCommand)
+ {
+ auto result = RunWslc(L"registry logout --help");
+ result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0});
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordAndStdinMutuallyExclusive)
+ {
+ auto result = RunWslc(L"login -u testuser -p testpass --password-stdin localhost:15099");
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"--password and --password-stdin are mutually exclusive") != std::wstring::npos);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordStdinRequiresUsername)
+ {
+ auto result = RunWslc(L"login --password-stdin localhost:15099");
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"Must provide --username with --password-stdin") != std::wstring::npos);
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Login_InvalidCredentials)
+ {
+ auto session = OpenDefaultElevatedSession();
+
+ {
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15003);
+ auto registryAddressW = string::MultiByteToWide(registryAddress);
+
+ // Login with wrong password should fail.
+ {
+ auto result =
+ RunWslc(std::format(L"login -u {} -p wrongpassword {}", string::MultiByteToWide(c_username), registryAddressW));
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos);
+ }
+
+ // Login with wrong username should fail.
+ {
+ auto result = RunWslc(std::format(L"login -u wronguser -p {} {}", string::MultiByteToWide(c_password), registryAddressW));
+ VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0));
+ VERIFY_IS_TRUE(result.Stderr.has_value());
+ VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos);
+ }
+
+ // Login with correct credentials should still succeed after failed attempts.
+ {
+ auto result = RunWslc(std::format(
+ L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW));
+ result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
+
+ VerifyLogoutSucceeds(registryAddressW);
+ }
+ }
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Registry_Login_CredentialInputMethods)
+ {
+ auto session = OpenDefaultElevatedSession();
+
+ {
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15002);
+ auto registryAddressW = string::MultiByteToWide(registryAddress);
+ auto usernameW = string::MultiByteToWide(c_username);
+ auto passwordW = string::MultiByteToWide(c_password);
+
+ // Login with -u and -p flags.
+ {
+ auto result = RunWslc(std::format(L"login -u {} -p {} {}", usernameW, passwordW, registryAddressW));
+ result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0});
+
+ VerifyLogoutSucceeds(registryAddressW);
+ }
+
+ // Login with -u and --password-stdin.
+ {
+ auto interactive = RunWslcInteractive(std::format(L"login -u {} --password-stdin {}", usernameW, registryAddressW));
+ interactive.WriteLine(c_password);
+ interactive.CloseStdin();
+ auto exitCode = interactive.Wait();
+ VERIFY_ARE_EQUAL(0, exitCode, L"Login with --password-stdin should succeed");
+
+ VerifyLogoutSucceeds(registryAddressW);
+ }
+
+ // Login with interactive prompts (no flags).
+ {
+ auto interactive = RunWslcInteractive(std::format(L"login {}", registryAddressW));
+ interactive.ExpectStderr("Username: ");
+ interactive.WriteLine(c_username);
+ interactive.ExpectStderr("Password: ");
+ interactive.WriteLine(c_password);
+ auto exitCode = interactive.Wait();
+ VERIFY_ARE_EQUAL(0, exitCode, L"Interactive login should succeed");
+
+ VerifyLogoutSucceeds(registryAddressW);
+ }
+ }
+ }
+
+private:
+ std::wstring GetLoginHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetLoginDescription() << GetLoginUsage() << GetLoginAvailableArguments() << GetLoginAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetLogoutHelpMessage() const
+ {
+ std::wstringstream output;
+ output << GetWslcHeader() << GetLogoutDescription() << GetLogoutUsage() << GetLogoutAvailableArguments()
+ << GetLogoutAvailableOptions();
+ return output.str();
+ }
+
+ std::wstring GetLoginDescription() const
+ {
+ return Localization::WSLCCLI_LoginLongDesc() + L"\r\n\r\n";
+ }
+
+ std::wstring GetLogoutDescription() const
+ {
+ return Localization::WSLCCLI_LogoutLongDesc() + L"\r\n\r\n";
+ }
+
+ std::wstring GetLoginUsage() const
+ {
+ return L"Usage: wslc registry login [] []\r\n\r\n";
+ }
+
+ std::wstring GetLogoutUsage() const
+ {
+ return L"Usage: wslc registry logout [] []\r\n\r\n";
+ }
+
+ std::wstring GetLoginAvailableArguments() const
+ {
+ std::wstringstream args;
+ args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
+ << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
+ << L"\r\n";
+ return args.str();
+ }
+
+ std::wstring GetLogoutAvailableArguments() const
+ {
+ std::wstringstream args;
+ args << Localization::WSLCCLI_AvailableArguments() << L"\r\n"
+ << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n"
+ << L"\r\n";
+ return args.str();
+ }
+
+ std::wstring GetLoginAvailableOptions() const
+ {
+ std::wstringstream options;
+ options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
+ << L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n"
+ << L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n"
+ << L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n"
+ << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n"
+ << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
+ << L"\r\n";
+ return options.str();
+ }
+
+ std::wstring GetLogoutAvailableOptions() const
+ {
+ std::wstringstream options;
+ options << Localization::WSLCCLI_AvailableOptions() << L"\r\n"
+ << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n"
+ << L"\r\n";
+ return options.str();
+ }
+};
+} // namespace WSLCE2ETests