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
2 changes: 1 addition & 1 deletion doc/windows/package-manager/winget/returnCodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ ms.localizationpriority: low
| 0x8A15007A | -1978335110 | APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE | Repair operation is not applicable. |
| 0x8A15007B | -1978335109 | APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED | Repair operation failed. |
| 0x8A15007C | -1978335108 | APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED | The installer technology in use doesn't support repair. |
| 0x8A15007D | -1978335107 | APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED | Repair operations involving administrator privileges are not permitted on packages installed within the user scope. |
| 0x8A15007D | -1978335107 | APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED | The requested operation is not permitted from an administrator context on packages installed within the user scope. |
| 0x8A15007E | -1978335106 | APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED | The SQLite connection was terminated to prevent corruption. |
| 0x8A15007F | -1978335105 | APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED | Failed to get Microsoft Store package catalog. |
| 0x8A150080 | -1978335104 | APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE | No applicable Microsoft Store package found from Microsoft Store package catalog. |
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotSpecified);
WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotSupported);
WINGET_DEFINE_RESOURCE_STRINGID(NoAdminRepairForUserScopePackage);
WINGET_DEFINE_RESOURCE_STRINGID(NoAdminUninstallForUserScopePackage);
WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers);
WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage);
WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledFontFound);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ namespace AppInstaller::CLI::Workflow

// When running as admin, block attempt to repair user scope installed package.
// [NOTE:] This check is to address the security concern related to above scenario.
if (Runtime::IsRunningAsAdmin())
if (Runtime::IsRunningWithNonDefaultFullToken())
{
auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>();
const std::string installedScopeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledScope];
Expand All @@ -386,7 +386,7 @@ namespace AppInstaller::CLI::Workflow
if (scopeEnum == ScopeEnum::User)
{
context.Reporter.Error() << Resource::String::NoAdminRepairForUserScopePackage << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED);
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED);
}
}

Expand Down
38 changes: 32 additions & 6 deletions src/AppInstallerCLICore/Workflows/UninstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ namespace AppInstaller::CLI::Workflow

std::vector<Item> Items;
};

bool AdminExecutionShouldBlockUserScopePackages(InstallerTypeEnum installerType)
{
switch (installerType)
{
case InstallerTypeEnum::Exe:
case InstallerTypeEnum::Burn:
case InstallerTypeEnum::Inno:
case InstallerTypeEnum::Nullsoft:
case InstallerTypeEnum::Portable:
return true;
default:
return false;
}
}
}

void UninstallSinglePackage(Execution::Context& context)
Expand Down Expand Up @@ -214,16 +229,27 @@ namespace AppInstaller::CLI::Workflow
return;
}

const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType];
switch (ConvertToInstallerTypeEnum(installedTypeString))
auto packageMetadata = installedPackageVersion->GetMetadata();
auto installedType = ConvertToInstallerTypeEnum(packageMetadata[PackageVersionMetadata::InstalledType]);

// When running as admin, block attempt to uninstall user scope package to prevent elevation of privilege paths.
if (AdminExecutionShouldBlockUserScopePackages(installedType) && Runtime::IsRunningWithNonDefaultFullToken())
{
auto scopeEnum = ConvertToScopeEnum(packageMetadata[PackageVersionMetadata::InstalledScope]);
if (scopeEnum == ScopeEnum::User)
Comment thread
JohnMcPMS marked this conversation as resolved.
{
context.Reporter.Error() << Resource::String::NoAdminUninstallForUserScopePackage << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED);
}
}

switch (installedType)
{
case InstallerTypeEnum::Exe:
case InstallerTypeEnum::Burn:
case InstallerTypeEnum::Inno:
case InstallerTypeEnum::Nullsoft:
{
IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata();

// Default to silent unless it is not present or interactivity is requested
auto uninstallCommandItr = packageMetadata.find(PackageVersionMetadata::SilentUninstallCommand);
if (uninstallCommandItr == packageMetadata.end() || context.Args.Contains(Execution::Args::Type::Interactive))
Expand Down Expand Up @@ -281,8 +307,8 @@ namespace AppInstaller::CLI::Workflow
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND);
}

const std::string installedScope = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope];
const std::string installedArch = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture];
const std::string installedScope = packageMetadata[Repository::PackageVersionMetadata::InstalledScope];
const std::string installedArch = packageMetadata[Repository::PackageVersionMetadata::InstalledArchitecture];

PortableInstaller portableInstaller = PortableInstaller(
Manifest::ConvertToScopeEnum(installedScope),
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLIE2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public class ErrorCode

public const int ERROR_NO_REPAIR_INFO_FOUND = unchecked((int)0x8A150079);
public const int ERROR_REPAIR_NOT_SUPPORTED = unchecked((int)0x8A15007C);
public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D);
public const int ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED = unchecked((int)0x8A15007D);

public const int ERROR_INSTALLER_ZERO_BYTE_FILE = unchecked((int)0x8A150086);
public const int ERROR_FONT_INSTALL_FAILED = unchecked((int)0x8A150087);
Expand Down
25 changes: 0 additions & 25 deletions src/AppInstallerCLIE2ETests/Interop/RepairInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,31 +148,6 @@ public async Task RepairBurnInstallerWithModifyBehavior()
Assert.That(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Modify Repair operation"), Is.True);
}

/// <summary>
/// Tests the repair operation of a Burn installer that was installed in user scope but is being repaired in an admin context.
/// </summary>
/// <returns>representing the asynchronous unit test.</returns>
[Test]
public async Task RepairBurnInstallerInAdminContextWithUserScopeInstall()
{
if (this.TestFactory.Context != ClsidContext.InProc)
{
Assert.Ignore("Test is only applicable for InProc context.");
}

string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestUserScopeInstallRepairInAdminContext");
var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestUserScopeInstallRepairInAdminContext", this.installDir, replaceInstallerArguments);

// Repair the package
var repairOptions = this.TestFactory.CreateRepairOptions();
repairOptions.PackageRepairMode = PackageRepairMode.Silent;

await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED);

// Cleanup
TestCommon.CleanupTestExeAndDirectory(this.installDir);
}

/// <summary>
/// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoModify ARP flag set.
/// </summary>
Expand Down
19 changes: 0 additions & 19 deletions src/AppInstallerCLIE2ETests/RepairCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,25 +120,6 @@ public void RepairBurnInstallerWithModifyBehavior()
Assert.That(TestCommon.VerifyTestExeRepairCompletedAndCleanup(installDir, "Modify Repair operation"), Is.True);
}

/// <summary>
/// Tests the repair operation of a Burn installer that was installed in user scope but is being repaired in an admin context.
/// </summary>
[Test]
public void RepairBurnInstallerInAdminContextWithUserScopeInstall()
{
// install a test burn package from TestSource and then repair it.
var installDir = TestCommon.GetRandomTestDir();
var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestUserScopeInstallRepairInAdminContext -v 2.0.0.0 --silent -l {installDir}");

Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.S_OK));
Assert.That(result.StdOut, Does.Contain("Successfully installed"));

result = TestCommon.RunAICLICommand("repair", "AppInstallerTest.TestUserScopeInstallRepairInAdminContext");
Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED));
Assert.That(result.StdOut, Does.Contain("The package installed for user scope cannot be repaired when running with administrator privileges."));
TestCommon.CleanupTestExeAndDirectory(installDir);
}

/// <summary>
/// Tests the repair operation of a Burn installer that lacks a repair behavior.
/// </summary>
Expand Down
7 changes: 5 additions & 2 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2890,8 +2890,8 @@ Please specify one of them using the --source option to proceed.</value>
<data name="NoAdminRepairForUserScopePackage" xml:space="preserve">
<value>The package installed for user scope cannot be repaired when running with administrator privileges.</value>
</data>
<data name="APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED" xml:space="preserve">
<value>Repair operations involving administrator privileges are not permitted on packages installed within the user scope.</value>
<data name="APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED" xml:space="preserve">
<value>The requested operation is not permitted from an administrator context on packages installed within the user scope.</value>
</data>
<data name="RepairFailedWithCode" xml:space="preserve">
<value>Repair failed with exit code: {0}</value>
Expand Down Expand Up @@ -3649,4 +3649,7 @@ An unlocalized JSON fragment will follow on another line.</comment>
<data name="WINGET_CONFIG_ERROR_PROCESSOR_HASH_MISMATCH" xml:space="preserve">
<value>The DSC processor hash provided does not match hash of the target file.</value>
</data>
<data name="NoAdminUninstallForUserScopePackage" xml:space="preserve">
<value>The package installed for user scope cannot be uninstalled when running with administrator privileges.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@
<ClCompile Include="PromptFlow.cpp" />
<ClCompile Include="Regex.cpp" />
<ClCompile Include="Registry.cpp" />
<ClCompile Include="RepairFlow.cpp" />
<ClCompile Include="Resources.cpp" />
<ClCompile Include="Rest.cpp" />
<ClCompile Include="RestClient.cpp" />
Expand Down
47 changes: 47 additions & 0 deletions src/AppInstallerCLITests/RepairFlow.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "TestHooks.h"
#include "TestSource.h"
#include "WorkflowCommon.h"
#include <winget/ManifestYamlParser.h>
#include <ExecutionContext.h>
#include <Workflows/ShellExecuteInstallerHandler.h>

using namespace TestCommon;
using namespace AppInstaller::CLI::Execution;
using namespace AppInstaller::Manifest;
using namespace AppInstaller::Repository;

// Tests that when the process is running with a non-default full (elevated UAC) token,
// repairing a package that was installed at user scope is blocked with
// APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED.
// This single test covers both the CLI (RepairCommand) and COM (COMRepairCommand) code paths
// because they both invoke the same ShellExecuteRepairImpl workflow step.
TEST_CASE("RepairFlow_AdminContextWithUserScopeInstall", "[RepairFlow][workflow]")
{
// Simulate running with an elevated (non-default full) token.
TestHook::SetIsRunningWithNonDefaultFullToken_Override tokenOverride(true);

std::ostringstream repairOutput;
TestContext context{ repairOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();

// Build an installed package version with Burn type installed at user scope.
// ShellExecuteRepairImpl blocks Burn (and other Exe-family) packages installed at user scope.
auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml"));
TestPackageVersion::MetadataMap metadata
{
{ PackageVersionMetadata::InstalledType, "Burn" },
{ PackageVersionMetadata::InstalledScope, "User" },
};
context.Add<Data::InstalledPackageVersion>(TestPackageVersion::Make(manifest, metadata));

// Provide a dummy repair string; the admin-context check fires before the command is executed.
context.Add<Data::RepairString>(std::string("C:\\repair.exe /silent"));

context << AppInstaller::CLI::Workflow::ShellExecuteRepairImpl;

INFO(repairOutput.str());
REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED);
}
17 changes: 17 additions & 0 deletions src/AppInstallerCLITests/TestHooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace AppInstaller
void TestHook_SetPathOverride(PathName target, const std::filesystem::path& path);
void TestHook_SetPathOverride(PathName target, const Filesystem::PathDetails& details);
void TestHook_ClearPathOverrides();
void TestHook_SetIsRunningWithNonDefaultFullToken_Override(bool* value);
}

namespace Repository
Expand Down Expand Up @@ -402,6 +403,22 @@ namespace TestHook
private:
};

struct SetIsRunningWithNonDefaultFullToken_Override
{
SetIsRunningWithNonDefaultFullToken_Override(bool value) : m_value(value)
{
AppInstaller::Runtime::TestHook_SetIsRunningWithNonDefaultFullToken_Override(&m_value);
}

~SetIsRunningWithNonDefaultFullToken_Override()
{
AppInstaller::Runtime::TestHook_SetIsRunningWithNonDefaultFullToken_Override(nullptr);
}

private:
bool m_value;
};

struct SetUserSettings_Override
{
SetUserSettings_Override(AppInstaller::Settings::UserSettings& settings)
Expand Down
44 changes: 44 additions & 0 deletions src/AppInstallerCLITests/UninstallFlow.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "TestHooks.h"
#include "WorkflowCommon.h"
#include <Commands/UninstallCommand.h>
#include <winget/ManifestYamlParser.h>
#include <Workflows/PortableFlow.h>
#include <Workflows/ShellExecuteInstallerHandler.h>
#include <Workflows/UninstallFlow.h>
Expand Down Expand Up @@ -210,3 +212,45 @@ TEST_CASE("UninstallFlow_UninstallMultiple_NotAllInstalled", "[UninstallFlow][wo

REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_QUERIES_FOUND_SINGLE);
}

TEST_CASE("UninstallFlow_AdminContextWithUserScopeInstall", "[UninstallFlow][workflow]")
{
// Simulate running with an elevated (non-default full) token.
TestHook::SetIsRunningWithNonDefaultFullToken_Override tokenOverride(true);

std::ostringstream uninstallOutput;
TestContext context{ uninstallOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();

// Create a test source with a Burn installer installed at user scope.
// AdminExecutionShouldBlockUserScopePackages returns true for Burn/Exe/Inno/Nullsoft/Portable.
auto testSource = CreateTestSource({
TestSourceResult(
"AppInstallerCliTest.TestBurnInstallerUserScope"sv,
[](std::vector<AppInstaller::Repository::ResultMatch>& matches, std::weak_ptr<const AppInstaller::Repository::ISource> source) {
auto manifest = AppInstaller::Manifest::YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml"));
matches.emplace_back(
AppInstaller::Repository::ResultMatch(
TestCompositePackage::Make(
manifest,
TestCompositePackage::MetadataMap
{
{ AppInstaller::Repository::PackageVersionMetadata::InstalledType, "Burn" },
{ AppInstaller::Repository::PackageVersionMetadata::InstalledScope, "User" },
},
std::vector<AppInstaller::Manifest::Manifest>{ manifest },
source
),
AppInstaller::Repository::PackageMatchFilter(AppInstaller::Repository::PackageMatchField::Id, AppInstaller::Repository::MatchType::Exact, "AppInstallerCliTest.TestBurnInstallerUserScope")));
})
});

OverrideForCompositeInstalledSource(context, testSource);
context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestBurnInstallerUserScope"sv);

UninstallCommand uninstall({});
uninstall.Execute(context);
INFO(uninstallOutput.str());

REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED);
}
2 changes: 1 addition & 1 deletion src/AppInstallerSharedLib/Errors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ namespace AppInstaller
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE, "Repair operation is not applicable."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED, "Repair operation failed."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED, "The installer technology in use doesn't support repair."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED, "Repair operations involving administrator privileges are not permitted on packages installed within the user scope."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED, "The requested operation is not permitted from an administrator context on packages installed within the user scope."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED, "The SQLite connection was terminated to prevent corruption."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED, "Failed to get Microsoft Store package catalog."),
WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE, "No applicable Microsoft Store package found from Microsoft Store package catalog."),
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerSharedLib/Public/AppInstallerErrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
#define APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE ((HRESULT)0x8A15007A)
#define APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED ((HRESULT)0x8A15007B)
#define APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED ((HRESULT)0x8A15007C)
#define APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED ((HRESULT)0x8A15007D)
#define APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED ((HRESULT)0x8A15007D)
#define APPINSTALLER_CLI_ERROR_SQLITE_CONNECTION_TERMINATED ((HRESULT)0x8A15007E)
#define APPINSTALLER_CLI_ERROR_DISPLAYCATALOG_API_FAILED ((HRESULT)0x8A15007F)
#define APPINSTALLER_CLI_ERROR_NO_APPLICABLE_DISPLAYCATALOG_PACKAGE ((HRESULT)0x8A150080)
Expand Down
Loading
Loading