diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e5a69dde6b..b1d4b4ebe2 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -307,6 +307,7 @@ mdmp MDs megamorf microsoftentraid +microsoftentraidforazureblobstorage midl minidump MINORVERSION diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e41f4536da..7940112a71 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,6 @@ pool: variables: solution: 'src\AppInstallerCLI.sln' - appxPackageDir: '$(Build.ArtifactStagingDirectory)/AppxPackages/' EnableDetectorVcpkg: true # Do not set the build version for a PR build. @@ -64,6 +63,7 @@ jobs: buildOutDir: $(Build.SourcesDirectory)\src\$(buildPlatform)\$(buildConfiguration) buildOutDirAnyCpu: $(Build.SourcesDirectory)\src\AnyCPU\$(buildConfiguration) artifactsDir: $(Build.ArtifactStagingDirectory)\$(buildPlatform) + appxPackageDir: $(Build.ArtifactStagingDirectory)\$(buildPlatform)\AppxPackages packageLayoutDir: $(Build.BinariesDirectory)\WingetPackageLayout steps: @@ -168,11 +168,11 @@ jobs: SourceFolder: '$(packageLayoutDir)' TargetFolder: '$(artifactsDir)\DevPackage' - - task: CopyFiles@2 - displayName: 'Copy Dev Packages' + - task: DeleteFiles@1 + displayName: Clean up Package Layout after copy inputs: - SourceFolder: '$(appxPackageDir)' - TargetFolder: '$(artifactsDir)\AppxPackages' + SourceFolder: '$(packageLayoutDir)' + Contents: '**/*' - task: CopyFiles@2 displayName: 'Copy native binaries for Microsoft.WinGet.Client (net8)' diff --git a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json index 98da3b2c34..62c878d96a 100644 --- a/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json +++ b/schemas/JSON/manifests/v1.10.0/manifest.installer.1.10.0.json @@ -601,6 +601,42 @@ "type": [ "boolean", "null" ], "description": "Indicates whether the install location should be added directly to the PATH environment variable. Only applies to an archive containing portable packages." }, + "Authentication": { + "type": [ "object", "null" ], + "properties": { + "AuthenticationType": { + "type": "string", + "enum": [ + "none", + "microsoftEntraId", + "microsoftEntraIdForAzureBlobStorage" + ], + "description": "The authentication type" + }, + "MicrosoftEntraIdAuthenticationInfo": { + "type": [ "object", "null" ], + "properties": { + "Resource": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The resource value for Microsoft Entra Id authentication." + }, + "Scope": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The scope value for Microsoft Entra Id authentication." + } + }, + "description": "The Microsoft Entra Id authentication info" + } + }, + "required": [ + "AuthenticationType" + ], + "description": "The authentication requirement for downloading the installer." + }, "Installer": { "type": "object", "properties": { @@ -724,6 +760,9 @@ }, "ArchiveBinariesDependOnPath": { "$ref": "#/definitions/ArchiveBinariesDependOnPath" + }, + "Authentication": { + "$ref": "#/definitions/Authentication" } }, "required": [ @@ -846,6 +885,9 @@ "ArchiveBinariesDependOnPath": { "$ref": "#/definitions/ArchiveBinariesDependOnPath" }, + "Authentication": { + "$ref": "#/definitions/Authentication" + }, "Installers": { "type": "array", "items": { diff --git a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json index dd48208c79..9f7c09cbb3 100644 --- a/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json +++ b/schemas/JSON/manifests/v1.10.0/manifest.singleton.1.10.0.json @@ -702,6 +702,42 @@ "type": [ "boolean", "null" ], "description": "Indicates whether the install location should be added directly to the PATH environment variable. Only applies to an archive containing portable packages." }, + "Authentication": { + "type": [ "object", "null" ], + "properties": { + "AuthenticationType": { + "type": "string", + "enum": [ + "none", + "microsoftEntraId", + "microsoftEntraIdForAzureBlobStorage" + ], + "description": "The authentication type" + }, + "MicrosoftEntraIdAuthenticationInfo": { + "type": [ "object", "null" ], + "properties": { + "Resource": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The resource value for Microsoft Entra Id authentication." + }, + "Scope": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "The scope value for Microsoft Entra Id authentication." + } + }, + "description": "The Microsoft Entra Id authentication info" + } + }, + "required": [ + "AuthenticationType" + ], + "description": "The authentication requirement for downloading the installer." + }, "Installer": { "type": "object", "properties": { @@ -825,6 +861,9 @@ }, "ArchiveBinariesDependOnPath": { "$ref": "#/definitions/ArchiveBinariesDependOnPath" + }, + "Authentication": { + "$ref": "#/definitions/Authentication" } }, "required": [ @@ -1070,6 +1109,9 @@ "ArchiveBinariesDependOnPath": { "$ref": "#/definitions/ArchiveBinariesDependOnPath" }, + "Authentication": { + "$ref": "#/definitions/Authentication" + }, "Installers": { "type": "array", "items": { diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 0b1864df3c..f8490ec1ef 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -277,9 +277,9 @@ namespace AppInstaller::CLI // Authentication arguments case Execution::Args::Type::AuthenticationMode: - return { type, "authentication-mode"_liv }; + return { type, "authentication-mode"_liv, ArgTypeCategory::CopyValueToSubContext }; case Execution::Args::Type::AuthenticationAccount: - return { type, "authentication-account"_liv }; + return { type, "authentication-account"_liv, ArgTypeCategory::CopyValueToSubContext }; // Used for demonstration purposes case Execution::Args::Type::ExperimentalArg: diff --git a/src/AppInstallerCLICore/Commands/COMCommand.cpp b/src/AppInstallerCLICore/Commands/COMCommand.cpp index 4af70ddc13..d50c47869d 100644 --- a/src/AppInstallerCLICore/Commands/COMCommand.cpp +++ b/src/AppInstallerCLICore/Commands/COMCommand.cpp @@ -20,7 +20,8 @@ namespace AppInstaller::CLI // IMPORTANT: To use this command, the caller should have already retrieved the package manifest (GetManifest()) and added it to the Context Data void COMDownloadCommand::ExecuteInternal(Context& context) const { - context << + context << + Workflow::InitializeInstallerDownloadAuthenticatorsMap << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << @@ -51,6 +52,7 @@ namespace AppInstaller::CLI void COMRepairCommand::ExecuteInternal(Execution::Context& context) const { context << + Workflow::InitializeInstallerDownloadAuthenticatorsMap << Workflow::SelectApplicableInstallerIfNecessary << Workflow::RepairSinglePackage; } diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp index bf42b837be..e06254427a 100644 --- a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp @@ -78,7 +78,9 @@ namespace AppInstaller::CLI void DownloadCommand::ExecuteInternal(Context& context) const { - context.SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly); + context.SetFlags(AppInstaller::CLI::Execution::ContextFlag::InstallerDownloadOnly); + + context << Workflow::InitializeInstallerDownloadAuthenticatorsMap; if (context.Args.Contains(Execution::Args::Type::Manifest)) { diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 744599c1d5..fefc0a6679 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "ImportCommand.h" +#include "ImportCommand.h" +#include "Workflows/DownloadFlow.h" #include "Workflows/CompletionFlow.h" #include "Workflows/ImportExportFlow.h" #include "Workflows/MultiQueryFlow.h" @@ -41,7 +42,8 @@ namespace AppInstaller::CLI void ImportCommand::ExecuteInternal(Execution::Context& context) const { - context << + context << + Workflow::InitializeInstallerDownloadAuthenticatorsMap << Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << Workflow::VerifyFile(Execution::Args::Type::ImportFile) << Workflow::ReadImportFile << diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 097719e70c..c93bf59a80 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -5,6 +5,7 @@ #include "CheckpointManager.h" #include "InstallCommand.h" #include "Workflows/CompletionFlow.h" +#include "Workflows/DownloadFlow.h" #include "Workflows/InstallFlow.h" #include "Workflows/UpdateFlow.h" #include "Workflows/MultiQueryFlow.h" @@ -118,6 +119,8 @@ namespace AppInstaller::CLI { context.SetFlags(ContextFlag::ShowSearchResultsOnPartialFailure); + context << Workflow::InitializeInstallerDownloadAuthenticatorsMap; + if (context.Args.Contains(Execution::Args::Type::Manifest)) { context << diff --git a/src/AppInstallerCLICore/Commands/RepairCommand.cpp b/src/AppInstallerCLICore/Commands/RepairCommand.cpp index 39543f527d..3df1a6a0e6 100644 --- a/src/AppInstallerCLICore/Commands/RepairCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RepairCommand.cpp @@ -4,6 +4,7 @@ #include "RepairCommand.h" #include "Workflows/RepairFlow.h" #include "Workflows/CompletionFlow.h" +#include "Workflows/DownloadFlow.h" #include "Workflows/InstallFlow.h" namespace AppInstaller::CLI @@ -94,6 +95,7 @@ namespace AppInstaller::CLI context.SetFlags(Execution::ContextFlag::InstallerExecutionUseRepair); context << + Workflow::InitializeInstallerDownloadAuthenticatorsMap << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::OpenSource() << Workflow::OpenCompositeSource(DetermineInstalledSource(context)); diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp index a5c2701ae2..e3dedc94e4 100644 --- a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "UpgradeCommand.h" #include "Workflows/CompletionFlow.h" +#include "Workflows/DownloadFlow.h" #include "Workflows/InstallFlow.h" #include "Workflows/MultiQueryFlow.h" #include "Workflows/UpdateFlow.h" @@ -157,6 +158,7 @@ namespace AppInstaller::CLI } context << + Workflow::InitializeInstallerDownloadAuthenticatorsMap << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::OpenSource() << Workflow::OpenCompositeSource(Workflow::DetermineInstalledSource(context)); diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp index 97d82f63ca..49789bb11d 100644 --- a/src/AppInstallerCLICore/ExecutionContext.cpp +++ b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -323,6 +323,7 @@ namespace AppInstaller::CLI::Execution clone->EnableSignalTerminationHandler(); } CopyArgsToSubContext(clone.get()); + CopyDataToSubContext(clone.get()); return clone; } @@ -342,6 +343,17 @@ namespace AppInstaller::CLI::Execution } } + void Context::CopyDataToSubContext(Context* subContext) + { +#define COPY_DATA_IF_EXISTS(dataType) \ + if (this->Contains(dataType)) \ + { \ + subContext->Add(this->Get()); \ + } + + COPY_DATA_IF_EXISTS(Data::InstallerDownloadAuthenticators); + } + void Context::EnableSignalTerminationHandler(bool enabled) { SetSignalTerminationHandlerContext(enabled, this); diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index a5fa557570..5223fdd972 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -192,6 +192,9 @@ namespace AppInstaller::CLI::Execution // Copies the args that are also needed in a sub-context. E.g., silent void CopyArgsToSubContext(Context* subContext); + // Copies the execution data that are also needed in a sub-context. E.g., shared installer download authenticator map + void CopyDataToSubContext(Context* subContext); + // Neither virtual functions nor member fields can be inside AICLI_DISABLE_TEST_HOOKS // or we could have ODR violations that lead to nasty bugs. So we will simply never // use this if AICLI_DISABLE_TEST_HOOKS is defined. diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 4633b05d33..1796ff5000 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include "CompletionData.h" @@ -67,6 +68,7 @@ namespace AppInstaller::CLI::Execution ModifyPath, RepairString, MsixDigests, + InstallerDownloadAuthenticators, Max }; @@ -290,5 +292,12 @@ namespace AppInstaller::CLI::Execution // The pair is { URL, Digest } using value_t = std::vector>; }; + + template<> + struct DataMapping + { + // The authenticator map shared with sub contexts + using value_t = std::shared_ptr>; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index cd56f587c2..aa37c797b1 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -302,8 +302,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstalledScopeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallerAbortsTerminal); WINGET_DEFINE_RESOURCE_STRINGID(InstallerBlockedByPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloadAuthenticationFailed); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloadAuthenticationNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloadCommandProhibited); WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloaded); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloadRequiresAuthentication); WINGET_DEFINE_RESOURCE_STRINGID(InstallerDownloads); WINGET_DEFINE_RESOURCE_STRINGID(InstallerElevationExpected); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index 084cbb4d3f..b933e50730 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -23,6 +23,12 @@ namespace AppInstaller::CLI::Workflow namespace { + constexpr std::string_view s_MicrosoftEntraIdAuthorizationHeader = "Authorization"sv; + // By default Azure blob storage does not accept Microsoft Entra Id authentication. + // https://learn.microsoft.com/en-us/rest/api/storageservices/versioning-for-the-azure-storage-services#authorize-requests-by-using-microsoft-entra-id-shared-key-or-shared-key-lite + constexpr std::string_view s_AzureBlobStorageApiVersionHeader = "x-ms-version"sv; + constexpr std::string_view s_AzureBlobStorageApiVersionValue = "2020-04-08"sv; + // Get the base download directory path for the installer. // Also creates the directory as necessary. std::filesystem::path GetInstallerBaseDownloadPath(Execution::Context& context) @@ -191,6 +197,66 @@ namespace AppInstaller::CLI::Workflow return false; } + + std::string GetInstallerDownloadAuthenticationToken(const AppInstaller::Authentication::AuthenticationInfo& authInfo, Execution::Context& context) + { + // First check if authenticator is already created + auto& authenticatorsMap = context.Get(); + auto authenticatorItr = authenticatorsMap->find(authInfo); + if (authenticatorItr == authenticatorsMap->end()) + { + AppInstaller::Authentication::Authenticator authenticator{ authInfo, GetAuthenticationArguments(context) }; + authenticatorsMap->emplace(authInfo, std::move(authenticator)); + } + + // Get the authenticator for auth. + authenticatorItr = authenticatorsMap->find(authInfo); + THROW_HR_IF(E_UNEXPECTED, authenticatorItr == authenticatorsMap->end()); + + auto authResult = authenticatorItr->second.AuthenticateForToken(); + if (FAILED(authResult.Status)) + { + AICLI_LOG(Repo, Error, << "Authentication failed for installer download. Result: " << authResult.Status); + THROW_HR_MSG(authResult.Status, "Failed to authenticate for installer download."); + } + + return authResult.Token; + } + + // Get additional headers for installer download request. Auth headers are acquired here. + std::vector GetInstallerDownloadAuthenticationHeaders(const AppInstaller::Manifest::ManifestInstaller& installer, Execution::Context& context) + { + std::vector result; + + switch (installer.AuthInfo.Type) + { + case AppInstaller::Authentication::AuthenticationType::None: + // No auth needed + break; + case AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId: + case AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage: + context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::InstallerDownloadRequiresAuthentication << std::endl; + result.push_back({ std::string{ s_MicrosoftEntraIdAuthorizationHeader }, Authentication::CreateBearerToken(GetInstallerDownloadAuthenticationToken(installer.AuthInfo, context)), true }); + if (installer.AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage) + { + result.push_back({ std::string{ s_AzureBlobStorageApiVersionHeader }, std::string{ s_AzureBlobStorageApiVersionValue }, false }); + } + break; + case AppInstaller::Authentication::AuthenticationType::Unknown: + default: + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED, "The package installer requires authentication that is not supported."); + } + + // Log result before return + std::string logMessage = "Installer download headers: "; + for (const auto& header : result) + { + logMessage += header.Name + ": " + (header.IsAuth ? "" : header.Value) + "; "; + } + AICLI_LOG(CLI, Info, << logMessage); + + return result; + } } void DownloadInstaller(Execution::Context& context) @@ -324,6 +390,26 @@ namespace AppInstaller::CLI::Workflow // Use the SHA256 hash of the installer as the identifier for the download downloadInfo.ContentId = SHA256::ConvertToString(installer.Sha256); + try + { + downloadInfo.RequestHeaders = GetInstallerDownloadAuthenticationHeaders(installer, context); + } + catch (const wil::ResultException& re) + { + AICLI_LOG(CLI, Error, << "Authentication failed for installer download. Error code: " << re.GetErrorCode()); + + if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED) + { + context.Reporter.Error() << Resource::String::InstallerDownloadAuthenticationNotSupported << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::InstallerDownloadAuthenticationFailed << std::endl; + } + + AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); + } + context.Reporter.Info() << Resource::String::Downloading << ' ' << Execution::UrlEmphasis << installer.Url << std::endl; DownloadResult downloadResult; @@ -686,4 +772,9 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED); } } + + void InitializeInstallerDownloadAuthenticatorsMap(Execution::Context& context) + { + context.Add(std::make_shared>()); + } } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.h b/src/AppInstallerCLICore/Workflows/DownloadFlow.h index ab7b448772..8a3b4ab8d4 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.h +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.h @@ -85,4 +85,11 @@ namespace AppInstaller::CLI::Workflow // Inputs: Installer // Outputs: None void EnsureSupportForDownload(Execution::Context& context); + + // This method initializes an empty InstallerDownloadAuthenticators map. + // InstallerDownloadAuthenticators map is for reusing authenticators when downloading multiple installers. + // Required Args: None + // Inputs: None + // Outputs: New empty InstallerDownloadAuthenticators + void InitializeInstallerDownloadAuthenticatorsMap(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/MultiQueryFlow.cpp b/src/AppInstallerCLICore/Workflows/MultiQueryFlow.cpp index 6ad4326013..ddb863a1b7 100644 --- a/src/AppInstallerCLICore/Workflows/MultiQueryFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/MultiQueryFlow.cpp @@ -40,7 +40,6 @@ namespace AppInstaller::CLI::Workflow auto& source = context.Get(); for (const auto& query : *context.Args.GetArgs(Execution::Args::Type::MultiQuery)) { - auto searchContextPtr = context.CreateSubContext(); Execution::Context& searchContext = *searchContextPtr; auto previousThreadGlobals = searchContext.SetForCurrentThread(); @@ -51,7 +50,6 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Creating search query for package [" << query << "]"); searchContext << GetSearchRequestForSingle; - packageSubContexts.emplace_back(std::move(searchContextPtr)); } @@ -148,5 +146,4 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(packageSubContexts)); } - -} \ No newline at end of file +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index c39f9a9aa7..2f870d9078 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2762,7 +2762,7 @@ Please specify one of them using the --source option to proceed. Specify the account to be used for authentication - Failed to add source. This winget version does not support the source's authentication method. Try upgrade to latest winget version. + Failed to add source. This winget version does not support the source's authentication method. Try upgrading to latest winget version. {Locked="winget"} @@ -3187,4 +3187,14 @@ Please specify one of them using the --source option to proceed. Configuration Modules PowerShell Modules that are used for the Configuration feature - \ No newline at end of file + + The package installer requires authentication. Authentication prompt may appear when necessary. Authenticated information will be shared with the installer download url. + + + Failed to download installer. This winget version does not support the installer download authentication method. Try upgrading to latest winget version. + {Locked="winget"} + + + Failed to download installer. Authentication failed. + + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 367e3108a2..5bf26983b7 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -817,6 +817,9 @@ true + + true + true @@ -1073,4 +1076,4 @@ - \ No newline at end of file + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 50ed22705d..5895890272 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -825,6 +825,9 @@ TestData + + TestData + TestData\MultiFileManifestV1 diff --git a/src/AppInstallerCLITests/DownloadFlow.cpp b/src/AppInstallerCLITests/DownloadFlow.cpp index 3e76830a33..c4e1a42f21 100644 --- a/src/AppInstallerCLITests/DownloadFlow.cpp +++ b/src/AppInstallerCLITests/DownloadFlow.cpp @@ -1,10 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "TestHooks.h" +#include "AppInstallerRuntime.h" #include "WorkflowCommon.h" #include using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Authentication; using namespace AppInstaller::CLI; TEST_CASE("DownloadFlow_DownloadCommandProhibited", "[DownloadFlow][workflow]") @@ -20,5 +24,118 @@ TEST_CASE("DownloadFlow_DownloadCommandProhibited", "[DownloadFlow][workflow]") // Verify AppInfo is printed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED); - REQUIRE(downloadOutput.str().find(Resource::LocString(Resource::String::InstallerDownloadCommandProhibited).get()) != std::string::npos); + REQUIRE(downloadOutput.str().find(CLI::Resource::LocString(CLI::Resource::String::InstallerDownloadCommandProhibited).get()) != std::string::npos); +} + +AppInstaller::Utility::DownloadResult ValidateAzureBlobStorageAuthHeaders( + const std::string&, + const std::filesystem::path& dest, + AppInstaller::Utility::DownloadType, + AppInstaller::IProgressCallback&, + std::optional info) +{ + REQUIRE(info); + REQUIRE(info->RequestHeaders.size() > 0); + REQUIRE(info->RequestHeaders[0].IsAuth); + REQUIRE(info->RequestHeaders[0].Name == "Authorization"); + REQUIRE(info->RequestHeaders[0].Value == "Bearer TestToken"); + REQUIRE_FALSE(info->RequestHeaders[1].IsAuth); + REQUIRE(info->RequestHeaders[1].Name == "x-ms-version"); + // Not validating x-ms-version value + + std::ofstream file(dest, std::ofstream::out); + file << "test"; + file.close(); + + AppInstaller::Utility::DownloadResult result; + result.Sha256Hash = AppInstaller::Utility::SHA256::ConvertToBytes("65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B"); + return result; +} + +TEST_CASE("DownloadFlow_DownloadWithInstallerAuthenticationSuccess", "[DownloadFlow][workflow]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + // Set authentication success result override + std::string expectedToken = "TestToken"; + AuthenticationResult authResultOverride; + authResultOverride.Status = S_OK; + authResultOverride.Token = expectedToken; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // Set auth header validation override + TestHook::SetDownloadResult_Function_Override downloadFunctionOverride({ &ValidateAzureBlobStorageAuthHeaders }); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("ManifestV1_10-InstallerAuthentication.yaml").GetPath().u8string()); + TestCommon::TempDirectory tempDirectory("TempDownload"); + context.Args.AddArg(Execution::Args::Type::DownloadDirectory, tempDirectory.GetPath().u8string()); + + DownloadCommand download({}); + download.Execute(context); + INFO(downloadOutput.str()); + + // Verify success + REQUIRE_FALSE(context.IsTerminated()); + REQUIRE(context.GetTerminationHR() == S_OK); +} + +TEST_CASE("DownloadFlow_DownloadWithInstallerAuthenticationNotSupported", "[DownloadFlow][workflow]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + // Set authentication failed result + AuthenticationResult authResultOverride; + authResultOverride.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("ManifestV1_10-InstallerAuthentication.yaml").GetPath().u8string()); + + DownloadCommand download({}); + download.Execute(context); + INFO(downloadOutput.str()); + + // Verify AppInfo is printed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); + REQUIRE(downloadOutput.str().find(CLI::Resource::LocString(CLI::Resource::String::InstallerDownloadAuthenticationNotSupported).get()) != std::string::npos); +} + +TEST_CASE("DownloadFlow_DownloadWithInstallerAuthenticationFailed", "[DownloadFlow][workflow]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + // Set authentication failed result + AuthenticationResult authResultOverride; + authResultOverride.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + std::ostringstream downloadOutput; + TestContext context{ downloadOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("ManifestV1_10-InstallerAuthentication.yaml").GetPath().u8string()); + + DownloadCommand download({}); + download.Execute(context); + INFO(downloadOutput.str()); + + // Verify AppInfo is printed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED); + REQUIRE(downloadOutput.str().find(CLI::Resource::LocString(CLI::Resource::String::InstallerDownloadAuthenticationFailed).get()) != std::string::npos); } diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp index a864454f76..6921a36d9d 100644 --- a/src/AppInstallerCLITests/RestClient.cpp +++ b/src/AppInstallerCLITests/RestClient.cpp @@ -264,6 +264,24 @@ TEST_CASE("GetInformation_Fail_InvalidMicrosoftEntraIdInfo", "[RestSource]") HttpClientHelper helper3{ GetTestRestRequestHandler(web::http::status_codes::OK, sample3) }; REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper3)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + + utility::string_t sample4 = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraIdForAzureBlobStorage" + } + }})delimiter"); + + HttpClientHelper helper4{ GetTestRestRequestHandler(web::http::status_codes::OK, sample4) }; + Authentication::AuthenticationArguments authArgs; + authArgs.Mode = Authentication::AuthenticationMode::Silent; + Version version_1_7{ "1.7.0" }; + REQUIRE_THROWS_HR(RestClient::GetSupportedInterface(TestRestUri, {}, RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper4)), authArgs, version_1_7, {}), APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); } TEST_CASE("RestClientCreate_UnsupportedVersion", "[RestSource]") diff --git a/src/AppInstallerCLITests/TestData/ManifestV1_10-InstallerAuthentication.yaml b/src/AppInstallerCLITests/TestData/ManifestV1_10-InstallerAuthentication.yaml new file mode 100644 index 0000000000..ac3ea15d98 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ManifestV1_10-InstallerAuthentication.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: AppInstallerCliTest.InstallerAuthentication +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer Authentication +Publisher: Microsoft Corporation +AppMoniker: AICLITestInstallerAuthentication +License: Test +Authentication: + AuthenticationType: microsoftEntraId + MicrosoftEntraIdAuthenticationInfo: + Resource: TestResource + Scope: TestScope + +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: msi + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + Authentication: + AuthenticationType: microsoftEntraIdForAzureBlobStorage +ManifestType: singleton +ManifestVersion: 1.10.0 diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index 3917c8d1c2..3d862c8270 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -93,6 +94,16 @@ namespace AppInstaller void SetLicensingHttpPipelineStage_Override(std::shared_ptr value); } + + namespace Utility::TestHooks + { + void SetDownloadResult_Function_Override(std::function info)>* value); + } } namespace TestHook @@ -301,4 +312,30 @@ namespace TestHook AppInstaller::MSStore::TestHooks::SetLicensingHttpPipelineStage_Override(nullptr); } }; + + struct SetDownloadResult_Function_Override + { + SetDownloadResult_Function_Override(std::function info)> value) : m_downloadFunction(std::move(value)) + { + AppInstaller::Utility::TestHooks::SetDownloadResult_Function_Override(&m_downloadFunction); + } + + ~SetDownloadResult_Function_Override() + { + AppInstaller::Utility::TestHooks::SetDownloadResult_Function_Override(nullptr); + } + + private: + std::function info)> m_downloadFunction; + }; } diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 65144a8d43..0bbe8243cc 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -1417,6 +1417,49 @@ TEST_CASE("WriteV1_10SingletonManifestAndVerifyContents", "[ManifestCreation]") VerifyV1ManifestContent(generatedMultiFileManifest, false, ManifestVer{ s_ManifestVersionV1_10 }, true); } +// Since Authentication is not supported in community repo and will cause manifest validation failure, +// we are not adding Authentication in v1_10 manifests. Instead a separate test is created for Authentication. +TEST_CASE("ReadWriteValidateV1_10ManifestWithInstallerAuthentication", "[ManifestValidation]") +{ + // Read manifest + TempDirectory testDirectory{ "TestManifest" }; + CopyTestDataFilesToFolder({ "ManifestV1_10-InstallerAuthentication.yaml" }, testDirectory); + Manifest testManifest = YamlParser::CreateFromPath(testDirectory); + + // Verify content + REQUIRE(testManifest.ManifestVersion == AppInstaller::Manifest::ManifestVer{ s_ManifestVersionV1_10 }); + REQUIRE(testManifest.DefaultInstallerInfo.AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId); + REQUIRE(testManifest.DefaultInstallerInfo.AuthInfo.MicrosoftEntraIdInfo); + REQUIRE(testManifest.DefaultInstallerInfo.AuthInfo.MicrosoftEntraIdInfo->Resource == "TestResource"); + REQUIRE(testManifest.DefaultInstallerInfo.AuthInfo.MicrosoftEntraIdInfo->Scope == "TestScope"); + REQUIRE(testManifest.Installers.size() == 1); + REQUIRE(testManifest.Installers[0].AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage); + REQUIRE(testManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo); + REQUIRE(testManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo->Resource == "https://storage.azure.com/"); + REQUIRE(testManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo->Scope.empty()); + + // Manifest Validation. Only error is "Authentication not supported". + auto errors = ValidateManifest(testManifest, true); + REQUIRE(errors.size() == 1); + REQUIRE(errors[0].GetErrorMessage() == "Field is not supported."); + REQUIRE(errors[0].Context == "Authentication"); + + // Write manifest + TempDirectory exportedDirectory{ "ExportedManifest" }; + std::filesystem::path exportedManifestPath = exportedDirectory.GetPath() / "ExportedManifest.yaml"; + YamlWriter::OutputYamlFile(testManifest, testManifest.Installers[0], exportedManifestPath); + + // Read back and validate content + REQUIRE(std::filesystem::exists(exportedManifestPath)); + Manifest exportedManifest = YamlParser::CreateFromPath(exportedDirectory); + REQUIRE(testManifest.ManifestVersion == AppInstaller::Manifest::ManifestVer{ s_ManifestVersionV1_10 }); + REQUIRE(exportedManifest.Installers.size() == 1); + REQUIRE(exportedManifest.Installers[0].AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage); + REQUIRE(exportedManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo); + REQUIRE(exportedManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo->Resource == "https://storage.azure.com/"); + REQUIRE(exportedManifest.Installers[0].AuthInfo.MicrosoftEntraIdInfo->Scope.empty()); +} + TEST_CASE("WriteManifestWithMultipleLocale", "[ManifestCreation]") { Manifest multiLocaleManifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-MultiLocale.yaml")); diff --git a/src/AppInstallerCommonCore/Authentication/Authentication.cpp b/src/AppInstallerCommonCore/Authentication/Authentication.cpp index ae9c8f121e..3d2e8067a4 100644 --- a/src/AppInstallerCommonCore/Authentication/Authentication.cpp +++ b/src/AppInstallerCommonCore/Authentication/Authentication.cpp @@ -12,7 +12,9 @@ namespace AppInstaller::Authentication { namespace { - const std::string c_BearerTokenPrefix = "Bearer "; + constexpr std::string_view s_BearerTokenPrefix = "Bearer "sv; + // Default Azure Blob Storage resource value. Used when manifest author did not provide specific blob resource. + constexpr std::string_view s_DefaultAzureBlobStorageResource = "https://storage.azure.com/"sv; } Authenticator::Authenticator(AuthenticationInfo info, AuthenticationArguments args) @@ -24,9 +26,9 @@ namespace AppInstaller::Authentication AICLI_LOG(Core, Info, << "AuthenticationArguments values. Mode: " << AuthenticationModeToString(args.Mode) << ", Account: " << args.AuthenticationAccount); - if (info.Type == AuthenticationType::MicrosoftEntraId) + if (info.Type == AuthenticationType::MicrosoftEntraId || info.Type == AuthenticationType::MicrosoftEntraIdForAzureBlobStorage) { - AICLI_LOG(Core, Info, << "Creating WebAccountManagerAuthenticator for MicrosoftEntraId"); + AICLI_LOG(Core, Info, << "Creating WebAccountManagerAuthenticator for " << AuthenticationTypeToString(info.Type)); m_authProvider = std::make_unique(std::move(info), std::move(args)); } } @@ -54,12 +56,48 @@ namespace AppInstaller::Authentication THROW_HR_IF(E_UNEXPECTED, !m_authProvider); return m_authProvider->AuthenticateForToken(); + } + + bool MicrosoftEntraIdAuthenticationInfo::operator<(const MicrosoftEntraIdAuthenticationInfo& other) const + { + // std::tie implements tuple comparison, wherein it checks the first item in the tuple, + // iff the first elements are equal, then the second element is used for comparison, and so on + return std::tie(Resource, Scope) < std::tie(other.Resource, other.Scope); + } + + bool AuthenticationInfo::operator<(const AuthenticationInfo& other) const + { + // std::tie implements tuple comparison, wherein it checks the first item in the tuple, + // iff the first elements are equal, then the second element is used for comparison, and so on + return std::tie(Type, MicrosoftEntraIdInfo) < std::tie(other.Type, other.MicrosoftEntraIdInfo); + } + + void AuthenticationInfo::UpdateRequiredFieldsIfNecessary() + { + // If MicrosoftEntraIdForAzureBlobStorage, populate default resource value if missing. + if (Type == AuthenticationType::MicrosoftEntraIdForAzureBlobStorage) + { + if (MicrosoftEntraIdInfo.has_value()) + { + if (MicrosoftEntraIdInfo->Resource.empty()) + { + MicrosoftEntraIdInfo->Resource = s_DefaultAzureBlobStorageResource; + MicrosoftEntraIdInfo->Scope = ""; + } + } + else + { + MicrosoftEntraIdAuthenticationInfo authInfo; + authInfo.Resource = s_DefaultAzureBlobStorageResource; + MicrosoftEntraIdInfo = std::move(authInfo); + } + } } - bool AuthenticationInfo::ValidateIntegrity() + bool AuthenticationInfo::ValidateIntegrity() const { // For MicrosoftEntraId, Resource is required. - if (Type == AuthenticationType::MicrosoftEntraId) + if (Type == AuthenticationType::MicrosoftEntraId || Type == AuthenticationType::MicrosoftEntraIdForAzureBlobStorage) { return MicrosoftEntraIdInfo.has_value() && !MicrosoftEntraIdInfo->Resource.empty(); } @@ -177,7 +215,9 @@ namespace AppInstaller::Authentication case AuthenticationType::None: return "none"sv; case AuthenticationType::MicrosoftEntraId: - return "microsoftEntraId"sv; + return "microsoftEntraId"sv; + case AuthenticationType::MicrosoftEntraIdForAzureBlobStorage: + return "microsoftEntraIdForAzureBlobStorage"sv; } return "unknown"sv; @@ -195,6 +235,10 @@ namespace AppInstaller::Authentication else if (inStrLower == "microsoftentraid") { result = AuthenticationType::MicrosoftEntraId; + } + else if (inStrLower == "microsoftentraidforazureblobstorage") + { + result = AuthenticationType::MicrosoftEntraIdForAzureBlobStorage; } return result; @@ -238,6 +282,6 @@ namespace AppInstaller::Authentication std::string AppInstaller::Authentication::CreateBearerToken(std::string rawToken) { - return c_BearerTokenPrefix + rawToken; + return std::string{ s_BearerTokenPrefix } + rawToken; } } diff --git a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp index f4bd39befb..bdef661041 100644 --- a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp +++ b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp @@ -31,7 +31,7 @@ namespace AppInstaller::Authentication THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !m_authInfo.ValidateIntegrity()); THROW_HR_IF(E_UNEXPECTED, m_authArgs.Mode == AuthenticationMode::Unknown); - if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + if (IsMicrosoftEntraIdAuthenticationType()) { m_webAccountProvider = WebAuthenticationCoreManager::FindAccountProviderAsync(s_MicrosoftEntraIdProviderId, s_MicrosoftEntraIdAuthority).get(); THROW_HR_IF_MSG(E_UNEXPECTED, !m_webAccountProvider, "Authentication Provider not found for Microsoft Entra Id"); @@ -108,7 +108,7 @@ namespace AppInstaller::Authentication WebAccount result = nullptr; - if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + if (IsMicrosoftEntraIdAuthenticationType()) { auto findAccountsResult = WebAuthenticationCoreManager::FindAllAccountsAsync(m_webAccountProvider, s_MicrosoftEntraIdClientId).get(); if (findAccountsResult.Status() == FindAllWebAccountsStatus::Success) @@ -144,7 +144,7 @@ namespace AppInstaller::Authentication { WebTokenRequest request = nullptr; - if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + if (IsMicrosoftEntraIdAuthenticationType()) { request = WebTokenRequest { @@ -283,5 +283,10 @@ namespace AppInstaller::Authentication AICLI_LOG(Core, Info, << "HandleGetTokenResult Result: " << result.Status); return result; + } + + bool WebAccountManagerAuthenticator::IsMicrosoftEntraIdAuthenticationType() + { + return m_authInfo.Type == AuthenticationType::MicrosoftEntraId || m_authInfo.Type == AuthenticationType::MicrosoftEntraIdForAzureBlobStorage; } } diff --git a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h index f6374a42a1..76829b5e42 100644 --- a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h +++ b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h @@ -26,5 +26,7 @@ namespace AppInstaller::Authentication AuthenticationResult GetToken(winrt::Windows::Security::Credentials::WebAccount webAccount, bool forceInteractive = false); AuthenticationResult GetTokenSilent(winrt::Windows::Security::Credentials::WebAccount webAccount); AuthenticationResult HandleGetTokenResult(winrt::Windows::Security::Authentication::Web::Core::WebTokenRequestResult requestResult); + + bool IsMicrosoftEntraIdAuthenticationType(); }; -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/DODownloader.cpp b/src/AppInstallerCommonCore/DODownloader.cpp index 988a0f754e..469edfcd3b 100644 --- a/src/AppInstallerCommonCore/DODownloader.cpp +++ b/src/AppInstallerCommonCore/DODownloader.cpp @@ -197,6 +197,21 @@ namespace AppInstaller::Utility SetUnknownProperty(DODownloadProperty_StreamInterface, streamInterface); } + void CustomHeaders(const std::vector& headers) + { + // DODownloadProperty_HttpCustomAuthHeaders is not used (does not work in our auth scenario). It is only used when challenged. + std::string customHeaders; + for (const auto& header : headers) + { + customHeaders += header.Name + ": " + header.Value + "\r\n"; + } + + if (!customHeaders.empty()) + { + SetProperty(DODownloadProperty_HttpCustomHeaders, customHeaders); + } + } + // Properties that may be interesting for future use: // https://docs.microsoft.com/en-us/windows/win32/delivery_optimization/deliveryoptimizationdownloadtypes/ne-deliveryoptimizationdownloadtypes-dodownloadproperty // - DODownloadProperty_CostPolicy :: Allow user to specify how to behave on metered networks @@ -431,6 +446,11 @@ namespace AppInstaller::Utility { download.ContentId(info->ContentId); } + + if (!info->RequestHeaders.empty()) + { + download.CustomHeaders(info->RequestHeaders); + } } download.Start(); diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index f96e0c0176..13c1be3b8c 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -102,10 +102,33 @@ namespace AppInstaller::Utility } } +#ifndef AICLI_DISABLE_TEST_HOOKS + namespace TestHooks + { + static std::function info)>* s_Download_Function_Override = nullptr; + + void SetDownloadResult_Function_Override(std::function info)>* value) + { + s_Download_Function_Override = value; + } + } +#endif + DownloadResult WinINetDownloadToStream( const std::string& url, std::ostream& dest, - IProgressCallback& progress) + IProgressCallback& progress, + std::optional info) { // For AICLI_LOG usages with string literals. #pragma warning(push) @@ -139,12 +162,22 @@ namespace AppInstaller::Utility THROW_LAST_ERROR_IF_NULL_MSG(session, "InternetOpen() failed."); + std::string customHeaders; + if (info && info->RequestHeaders.size() > 0) + { + for (const auto& header : info->RequestHeaders) + { + customHeaders += header.Name + ": " + header.Value + "\r\n"; + } + } + std::wstring customHeadersWide = Utility::ConvertToUTF16(customHeaders); + auto urlWide = Utility::ConvertToUTF16(url); wil::unique_hinternet urlFile(InternetOpenUrl( session.get(), urlWide.c_str(), - NULL, - 0, + customHeadersWide.empty() ? NULL : customHeadersWide.c_str(), + customHeadersWide.empty() ? 0 : (DWORD)(customHeadersWide.size()), INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS, // This allows http->https redirection 0)); THROW_LAST_ERROR_IF_NULL_MSG(urlFile, "InternetOpenUrl() failed."); @@ -296,10 +329,10 @@ namespace AppInstaller::Utility std::ostream& dest, DownloadType, IProgressCallback& progress, - std::optional) + std::optional info) { THROW_HR_IF(E_INVALIDARG, url.empty()); - return WinINetDownloadToStream(url, dest, progress); + return WinINetDownloadToStream(url, dest, progress, info); } DownloadResult Download( @@ -309,6 +342,13 @@ namespace AppInstaller::Utility IProgressCallback& progress, std::optional info) { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (TestHooks::s_Download_Function_Override) + { + return (*TestHooks::s_Download_Function_Override)(url, dest, type, progress, info); + } +#endif + THROW_HR_IF(E_INVALIDARG, url.empty()); THROW_HR_IF(E_INVALIDARG, dest.empty()); @@ -368,7 +408,7 @@ namespace AppInstaller::Utility // Use std::ofstream::app to append to previous empty file so that it will not // create a new file and clear motw. std::ofstream outfile(dest, std::ofstream::binary | std::ofstream::app); - return WinINetDownloadToStream(url, outfile, progress); + return WinINetDownloadToStream(url, outfile, progress, info); } using namespace std::string_view_literals; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 4b1da4fb92..d045a88292 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -347,6 +347,21 @@ namespace AppInstaller::Manifest } } } + + // Check AuthInfo validity. For full validation (community repo), authentication type must be none. + if (installer.AuthInfo.Type != Authentication::AuthenticationType::None) + { + if (fullValidation) + { + // Authentication is not supported (must be none) in community repo. + resultErrors.emplace_back(ManifestError::FieldNotSupported, "Authentication"); + } + + if (!installer.AuthInfo.ValidateIntegrity()) + { + resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Authentication"); + } + } } // Validate localizations diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index ce448c09f9..2d46f60403 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -388,6 +388,16 @@ namespace AppInstaller::Manifest std::move(fields_v1_9.begin(), fields_v1_9.end(), std::inserter(result, result.end())); } + + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_10 }) + { + std::vector fields_v1_10 = + { + { "Authentication", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestInstallerPtr(v)->AuthInfo = {}; auto errors = ValidateAndProcessFields(value, AuthenticationFieldInfos, VariantManifestPtr(&(GetManifestInstallerPtr(v)->AuthInfo))); GetManifestInstallerPtr(v)->AuthInfo.UpdateRequiredFieldsIfNecessary(); return errors; }, true}, + }; + + std::move(fields_v1_10.begin(), fields_v1_10.end(), std::inserter(result, result.end())); + } } return result; @@ -716,7 +726,38 @@ namespace AppInstaller::Manifest return result; } - + std::vector ManifestYamlPopulator::GetAuthenticationFieldInfos() + { + std::vector result = {}; + + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_10 }) + { + result = + { + { "AuthenticationType", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Type = Authentication::ConvertToAuthenticationType(value.as()); return {}; } }, + { "MicrosoftEntraIdAuthenticationInfo", [this](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->MicrosoftEntraIdInfo.emplace(); return ValidateAndProcessFields(value, MicrosoftEntraIdAuthenticationInfoFieldInfos, VariantManifestPtr(&(variant_ptr(v)->MicrosoftEntraIdInfo.value()))); }}, + }; + } + + return result; + } + + std::vector ManifestYamlPopulator::GetMicrosoftEntraIdAuthenticationInfoFieldInfos() + { + std::vector result = {}; + + if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_10 }) + { + result = + { + { "Resource", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Resource = Utility::Trim(value.as()); return {}; } }, + { "Scope", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { variant_ptr(v)->Scope = Utility::Trim(value.as()); return {}; } }, + }; + } + + return result; + } + std::vector ManifestYamlPopulator::GetShadowRootFieldProcessInfo() { std::vector result; @@ -1071,6 +1112,8 @@ namespace AppInstaller::Manifest NestedInstallerFileFieldInfos = GetNestedInstallerFileFieldProcessInfo(); InstallationMetadataFieldInfos = GetInstallationMetadataFieldProcessInfo(); InstallationMetadataFilesFieldInfos = GetInstallationMetadataFilesFieldProcessInfo(); + AuthenticationFieldInfos = GetAuthenticationFieldInfos(); + MicrosoftEntraIdAuthenticationInfoFieldInfos = GetMicrosoftEntraIdAuthenticationInfoFieldInfos(); resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, VariantManifestPtr(&(m_manifest.get()))); diff --git a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp index 62bd3f8d2a..4483fbdaba 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlWriter.cpp @@ -67,7 +67,12 @@ namespace AppInstaller::Manifest::YamlWriter constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; constexpr std::string_view DownloadCommandProhibited = "DownloadCommandProhibited"sv; constexpr std::string_view RepairBehavior = "RepairBehavior"sv; - constexpr std::string_view ArchiveBinariesDependOnPath = "ArchiveBinariesDependOnPath"sv; + constexpr std::string_view ArchiveBinariesDependOnPath = "ArchiveBinariesDependOnPath"sv; + constexpr std::string_view Authentication = "Authentication"sv; + constexpr std::string_view AuthenticationType = "AuthenticationType"sv; + constexpr std::string_view MicrosoftEntraIdAuthenticationInfo = "MicrosoftEntraIdAuthenticationInfo"sv; + constexpr std::string_view MicrosoftEntraIdResource = "Resource"sv; + constexpr std::string_view MicrosoftEntraIdScope = "Scope"sv; // Installer switches constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; @@ -494,6 +499,27 @@ namespace AppInstaller::Manifest::YamlWriter WRITE_PROPERTY_IF_EXISTS(out, DefaultInstallLocation, installationMetadata.DefaultInstallLocation); ProcessInstallationMetadataInstalledFiles(out, installationMetadata.Files); out << YAML::EndMap; + } + + void ProcessAuthentication(YAML::Emitter& out, const Authentication::AuthenticationInfo& authInfo) + { + if (authInfo.Type == Authentication::AuthenticationType::None) + { + return; + } + + out << YAML::Key << Authentication; + out << YAML::BeginMap; + WRITE_PROPERTY(out, AuthenticationType, Authentication::AuthenticationTypeToString(authInfo.Type)); + if (authInfo.MicrosoftEntraIdInfo) + { + out << YAML::Key << MicrosoftEntraIdAuthenticationInfo; + out << YAML::BeginMap; + WRITE_PROPERTY_IF_EXISTS(out, MicrosoftEntraIdResource, authInfo.MicrosoftEntraIdInfo->Resource); + WRITE_PROPERTY_IF_EXISTS(out, MicrosoftEntraIdScope, authInfo.MicrosoftEntraIdInfo->Scope); + out << YAML::EndMap; + } + out << YAML::EndMap; } void ProcessDependencies(YAML::Emitter& out, const DependencyList& dependencies) @@ -604,7 +630,8 @@ namespace AppInstaller::Manifest::YamlWriter ProcessNestedInstallerFiles(out, installer.NestedInstallerFiles); ProcessPlatforms(out, installer.Platform); ProcessUnsupportedArguments(out, installer.UnsupportedArguments); - ProcessUnsupportedOSArchitecture(out, installer.UnsupportedOSArchitectures); + ProcessUnsupportedOSArchitecture(out, installer.UnsupportedOSArchitectures); + ProcessAuthentication(out, installer.AuthInfo); } void ProcessInstaller(YAML::Emitter& out, const ManifestInstaller& installer) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h index 1c71b148dd..1ceb4419dd 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -29,11 +29,20 @@ namespace AppInstaller::Utility ConfigurationFile, }; + struct DownloadRequestHeader + { + std::string Name; + std::string Value; + bool IsAuth = false; + }; + // Extra metadata about a download for use by certain downloaders (Delivery Optimization for instance). + // Extra download request headers. struct DownloadInfo { std::string DisplayName; std::string ContentId; + std::vector RequestHeaders; }; // Properties about the downloaded file. diff --git a/src/AppInstallerCommonCore/Public/winget/Authentication.h b/src/AppInstallerCommonCore/Public/winget/Authentication.h index 5d16f5249f..1d7ee5a0e8 100644 --- a/src/AppInstallerCommonCore/Public/winget/Authentication.h +++ b/src/AppInstallerCommonCore/Public/winget/Authentication.h @@ -14,6 +14,7 @@ namespace AppInstaller::Authentication Unknown, None, MicrosoftEntraId, + MicrosoftEntraIdForAzureBlobStorage, }; std::string_view AuthenticationTypeToString(AuthenticationType in); @@ -45,6 +46,8 @@ namespace AppInstaller::Authentication // Scope is optional std::string Scope; + + bool operator<(const MicrosoftEntraIdAuthenticationInfo& other) const; }; // Authentication info struct used to initialize Authenticator, this is from source information. @@ -53,8 +56,13 @@ namespace AppInstaller::Authentication AuthenticationType Type = AuthenticationType::None; std::optional MicrosoftEntraIdInfo; + bool operator<(const AuthenticationInfo& other) const; + + // Update default values for missing required fields for known authentication type. + void UpdateRequiredFieldsIfNecessary(); + // Validates data integrity against known authentication type. - bool ValidateIntegrity(); + bool ValidateIntegrity() const; }; // Authentication arguments struct used to initialize Authenticator, this is from user input. @@ -131,4 +139,4 @@ namespace AppInstaller::Authentication // Create bearer token from a raw token std::string CreateBearerToken(std::string rawToken); -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h index e80d076f9c..9812933410 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -3,7 +3,8 @@ #pragma once #include #include -#include +#include +#include #include #include @@ -117,5 +118,7 @@ namespace AppInstaller::Manifest bool DownloadCommandProhibited = false; bool ArchiveBinariesDependOnPath = false; + + Authentication::AuthenticationInfo AuthInfo; }; } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h index 4c79f7e3f5..db7775afe2 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -8,7 +8,7 @@ namespace AppInstaller::Manifest { // Add here new manifest pointer types. - using VariantManifestPtr = std::variant*>; + using VariantManifestPtr = std::variant*, AppInstaller::Authentication::AuthenticationInfo*, AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo*>; struct ManifestYamlPopulator { @@ -55,6 +55,8 @@ namespace AppInstaller::Manifest std::vector NestedInstallerFileFieldInfos; std::vector InstallationMetadataFieldInfos; std::vector InstallationMetadataFilesFieldInfos; + std::vector AuthenticationFieldInfos; + std::vector MicrosoftEntraIdAuthenticationInfoFieldInfos; // Cache of Installers node and Localization node YAML::Node const* m_p_installersNode = nullptr; @@ -75,6 +77,8 @@ namespace AppInstaller::Manifest std::vector GetNestedInstallerFileFieldProcessInfo(); std::vector GetInstallationMetadataFieldProcessInfo(); std::vector GetInstallationMetadataFilesFieldProcessInfo(); + std::vector GetAuthenticationFieldInfos(); + std::vector GetMicrosoftEntraIdAuthenticationInfoFieldInfos(); // Shadow std::vector GetShadowRootFieldProcessInfo(); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp index d077a8151a..de4819f28e 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp @@ -14,6 +14,33 @@ namespace AppInstaller::Repository::Rest::Schema constexpr std::string_view MicrosoftEntraIdAuthenticationInfo = "MicrosoftEntraIdAuthenticationInfo"sv; constexpr std::string_view MicrosoftEntraId_Resource = "Resource"sv; constexpr std::string_view MicrosoftEntraId_Scope = "Scope"sv; + + Authentication::AuthenticationType ConvertToAuthenticationTypeForSource(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + Authentication::AuthenticationType result = Authentication::AuthenticationType::Unknown; + + if (inStrLower == "none") + { + result = Authentication::AuthenticationType::None; + } + else if (inStrLower == "microsoftentraid") + { + result = Authentication::AuthenticationType::MicrosoftEntraId; + } + + return result; + } + + Authentication::AuthenticationType ConvertToAuthenticationTypeForInstaller(std::string_view in, Manifest::ManifestVer manifestVersion) + { + if (manifestVersion >= Manifest::ManifestVer{ Manifest::s_ManifestVersionV1_10 }) + { + return Authentication::ConvertToAuthenticationType(in); + } + + return Authentication::AuthenticationType::Unknown; + } } // The authentication info json looks like below: @@ -24,7 +51,7 @@ namespace AppInstaller::Repository::Rest::Schema // "Scope" : "test" // } // } - Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, std::optional) + Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, ParseAuthenticationInfoType parseType, std::optional manifestVersion) { auto authenticationObject = JSON::GetJsonValueFromNode(dataObject, JSON::GetUtilityString(Authentication)); if (!authenticationObject) @@ -46,7 +73,15 @@ namespace AppInstaller::Repository::Rest::Schema auto authenticationTypeString = JSON::GetRawStringValueFromJsonNode(authenticationObjectNode, JSON::GetUtilityString(AuthenticationType)); // AuthenticationType required if Authentication exists and is not null. THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !JSON::IsValidNonEmptyStringValue(authenticationTypeString)); - result.Type = Authentication::ConvertToAuthenticationType(authenticationTypeString.value()); + if (parseType == ParseAuthenticationInfoType::Source) + { + result.Type = ConvertToAuthenticationTypeForSource(authenticationTypeString.value()); + } + else if (parseType == ParseAuthenticationInfoType::Installer) + { + THROW_HR_IF(E_INVALIDARG, !manifestVersion); + result.Type = ConvertToAuthenticationTypeForInstaller(authenticationTypeString.value(), manifestVersion.value()); + } // Parse MicrosoftEntraId info auto microsoftEntraIdInfoObject = JSON::GetJsonValueFromNode(authenticationObjectNode, JSON::GetUtilityString(MicrosoftEntraIdAuthenticationInfo)); @@ -70,6 +105,7 @@ namespace AppInstaller::Repository::Rest::Schema result.MicrosoftEntraIdInfo = std::move(microsoftEntraIdInfo); } + result.UpdateRequiredFieldsIfNecessary(); THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !result.ValidateIntegrity()); return result; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h index 7c8c8758d8..ce68d4bcdc 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h +++ b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h @@ -7,6 +7,12 @@ namespace AppInstaller::Repository::Rest::Schema { + enum class ParseAuthenticationInfoType + { + Source, + Installer, + }; + // Parses AuthenticationInfo from json object. // This could be used for installer level parsing as well in manifest deserializer (currently not supported, manifestVersion not used). // The authentication info json looks like below: @@ -17,5 +23,5 @@ namespace AppInstaller::Repository::Rest::Schema // "Scope" : "test" // } // } - Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, std::optional manifestVersion = {}); + Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, ParseAuthenticationInfoType parseType, std::optional manifestVersion = {}); } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp index 6a272de037..5d67cc6b90 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp @@ -124,7 +124,7 @@ namespace AppInstaller::Repository::Rest::Schema info.RequiredQueryParameters = JSON::GetRawStringArrayFromJsonNode(dataValue, JSON::GetUtilityString(RequiredQueryParameters)); info.UnsupportedQueryParameters = JSON::GetRawStringArrayFromJsonNode(dataValue, JSON::GetUtilityString(UnsupportedQueryParameters)); - info.Authentication = ParseAuthenticationInfo(dataValue); + info.Authentication = ParseAuthenticationInfo(dataValue, ParseAuthenticationInfoType::Source); return info; } diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 08338dd306..41f796d272 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -464,6 +464,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation return Microsoft::Management::Deployment::AuthenticationType::None; case ::AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId: return Microsoft::Management::Deployment::AuthenticationType::MicrosoftEntraId; + case ::AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage: + return Microsoft::Management::Deployment::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage; } return Microsoft::Management::Deployment::AuthenticationType::Unknown; diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.cpp b/src/Microsoft.Management.Deployment/DownloadOptions.cpp index fbd73bc936..4a8c7317c7 100644 --- a/src/Microsoft.Management.Deployment/DownloadOptions.cpp +++ b/src/Microsoft.Management.Deployment/DownloadOptions.cpp @@ -97,6 +97,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation void DownloadOptions::CorrelationData(hstring const& value) { m_correlationData = value; + } + winrt::Microsoft::Management::Deployment::AuthenticationArguments DownloadOptions::AuthenticationArguments() + { + return m_authenticationArguments; + } + void DownloadOptions::AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value) + { + m_authenticationArguments = value; } CoCreatableMicrosoftManagementDeploymentClass(DownloadOptions); } diff --git a/src/Microsoft.Management.Deployment/DownloadOptions.h b/src/Microsoft.Management.Deployment/DownloadOptions.h index 77530007f9..bc0f95d7f3 100644 --- a/src/Microsoft.Management.Deployment/DownloadOptions.h +++ b/src/Microsoft.Management.Deployment/DownloadOptions.h @@ -30,7 +30,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool AcceptPackageAgreements(); void AcceptPackageAgreements(bool value); hstring CorrelationData(); - void CorrelationData(hstring const& value); + void CorrelationData(hstring const& value); + winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); + void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -43,7 +45,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool m_allowHashMismatch = false; bool m_skipDependencies = false; bool m_acceptPackageAgreements = true; - std::wstring m_correlationData = L""; + std::wstring m_correlationData = L""; + winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; #endif }; } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.cpp b/src/Microsoft.Management.Deployment/InstallOptions.cpp index 3d491a29e5..09189fd76e 100644 --- a/src/Microsoft.Management.Deployment/InstallOptions.cpp +++ b/src/Microsoft.Management.Deployment/InstallOptions.cpp @@ -160,5 +160,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return m_skipDependencies; } + winrt::Microsoft::Management::Deployment::AuthenticationArguments InstallOptions::AuthenticationArguments() + { + return m_authenticationArguments; + } + void InstallOptions::AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value) + { + m_authenticationArguments = value; + } CoCreatableMicrosoftManagementDeploymentClass(InstallOptions); } diff --git a/src/Microsoft.Management.Deployment/InstallOptions.h b/src/Microsoft.Management.Deployment/InstallOptions.h index b5f2fb0a6e..15913bf5ce 100644 --- a/src/Microsoft.Management.Deployment/InstallOptions.h +++ b/src/Microsoft.Management.Deployment/InstallOptions.h @@ -44,6 +44,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation void AcceptPackageAgreements(bool value); bool SkipDependencies(); void SkipDependencies(bool value); + winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); + void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -65,6 +67,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool m_force = false; bool m_acceptPackageAgreements = true; bool m_skipDependencies = false; + winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp b/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp index 01f55b9df2..f6e53ecd04 100644 --- a/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp +++ b/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp @@ -2,7 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "PackageInstallerInfo.h" -#include "PackageInstallerInfo.g.cpp" +#include "PackageInstallerInfo.g.cpp" +#include "AuthenticationInfo.h" #include "Converters.h" #include @@ -37,4 +38,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return GetDeploymentElevationRequirement(m_manifestInstaller.ElevationRequirement); } + winrt::Microsoft::Management::Deployment::AuthenticationInfo PackageInstallerInfo::AuthenticationInfo() + { + std::call_once(m_authenticationInfoOnceFlag, + [&]() + { + auto authenticationInfo = winrt::make_self>(); + authenticationInfo->Initialize(m_manifestInstaller.AuthInfo); + m_authenticationInfo = *authenticationInfo; + }); + return m_authenticationInfo; + } } diff --git a/src/Microsoft.Management.Deployment/PackageInstallerInfo.h b/src/Microsoft.Management.Deployment/PackageInstallerInfo.h index 6a0021b6cf..de09dd32bd 100644 --- a/src/Microsoft.Management.Deployment/PackageInstallerInfo.h +++ b/src/Microsoft.Management.Deployment/PackageInstallerInfo.h @@ -20,11 +20,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageInstallerScope Scope(); hstring Locale(); // Contract 6.0 - winrt::Microsoft::Management::Deployment::ElevationRequirement ElevationRequirement(); - + winrt::Microsoft::Management::Deployment::ElevationRequirement ElevationRequirement(); + // Contract 12.0 + winrt::Microsoft::Management::Deployment::AuthenticationInfo AuthenticationInfo(); + #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: - ::AppInstaller::Manifest::ManifestInstaller m_manifestInstaller; + ::AppInstaller::Manifest::ManifestInstaller m_manifestInstaller; + std::once_flag m_authenticationInfoOnceFlag; + winrt::Microsoft::Management::Deployment::AuthenticationInfo m_authenticationInfo{ nullptr }; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index e0b3c7c87d..836a31d0c9 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -7,6 +7,7 @@ #include "ComContext.h" #include "ExecutionContext.h" #include "Workflows/WorkflowBase.h" +#include #include #include #include "Commands/COMCommand.h" @@ -572,6 +573,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation { context->Args.AddArg(Execution::Args::Type::SkipDependencies); } + + if (options.AuthenticationArguments()) + { + context->Args.AddArg(Execution::Args::Type::AuthenticationMode, ::AppInstaller::Authentication::AuthenticationModeToString(GetAuthenticationMode(options.AuthenticationArguments().AuthenticationMode()))); + context->Args.AddArg(Execution::Args::Type::AuthenticationAccount, ::AppInstaller::Utility::ConvertToUTF8(options.AuthenticationArguments().AuthenticationAccount())); + } } else { @@ -660,6 +667,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation { context->Args.AddArg(Execution::Args::Type::InstallerType, AppInstaller::Manifest::InstallerTypeToString(installerType)); } + + if (options.AuthenticationArguments()) + { + context->Args.AddArg(Execution::Args::Type::AuthenticationMode, ::AppInstaller::Authentication::AuthenticationModeToString(GetAuthenticationMode(options.AuthenticationArguments().AuthenticationMode()))); + context->Args.AddArg(Execution::Args::Type::AuthenticationAccount, ::AppInstaller::Utility::ConvertToUTF8(options.AuthenticationArguments().AuthenticationAccount())); + } } } @@ -709,6 +722,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation { context->Args.AddArg(Execution::Args::Type::InstallScope, ScopeToString(repairScope)); } + + if (options.AuthenticationArguments()) + { + context->Args.AddArg(Execution::Args::Type::AuthenticationMode, ::AppInstaller::Authentication::AuthenticationModeToString(GetAuthenticationMode(options.AuthenticationArguments().AuthenticationMode()))); + context->Args.AddArg(Execution::Args::Type::AuthenticationAccount, ::AppInstaller::Utility::ConvertToUTF8(options.AuthenticationArguments().AuthenticationAccount())); + } } } diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 943177fc45..29f2e9166a 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -515,6 +515,12 @@ namespace Microsoft.Management.Deployment /// The package installer elevation requirement. ElevationRequirement ElevationRequirement { get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 12)] + { + /// Authentication info from the package installer. + AuthenticationInfo AuthenticationInfo { get; }; + } }; /// The installed status type. The values need to match InstalledStatusType from winget/RepositorySearch.h. @@ -813,6 +819,10 @@ namespace Microsoft.Management.Deployment Unknown, None, MicrosoftEntraId, + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 12)] + { + MicrosoftEntraIdForAzureBlobStorage, + } }; /// Microsoft Entra Id related authentication info. @@ -927,7 +937,6 @@ namespace Microsoft.Management.Deployment [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] { - /// Authentication arguments used in authentication flow during package catalog operations if applicable. /// This is user or caller input. AuthenticationArguments AuthenticationArguments; @@ -1047,11 +1056,11 @@ namespace Microsoft.Management.Deployment [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 2)] { - // The set of allowed Architectures, in preference order, that will be considered for - // the install operation. Initially the vector contains the default allowed architectures - // in the default preference order for the current system. It is allowed to have repeated - // values in the list, to make prepending a preference override easier. Instances of an - // architecture after the first will simply be ignored. + /// The set of allowed Architectures, in preference order, that will be considered for + /// the install operation. Initially the vector contains the default allowed architectures + /// in the default preference order for the current system. It is allowed to have repeated + /// values in the list, to make prepending a preference override easier. Instances of an + /// architecture after the first will simply be ignored. Windows.Foundation.Collections.IVector AllowedArchitectures { get; }; } @@ -1082,12 +1091,18 @@ namespace Microsoft.Management.Deployment [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 7)] { - // Skip installing the dependencies for the package. + /// Skip installing the dependencies for the package. Boolean SkipDependencies; /// The package installer type. PackageInstallerType InstallerType; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 12)] + { + /// Authentication arguments used when downloading the package installer if authentication is required. + AuthenticationArguments AuthenticationArguments; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 4)] @@ -1181,6 +1196,12 @@ namespace Microsoft.Management.Deployment /// Used by a caller to correlate the download with a caller's data. /// The string must be JSON encoded. String CorrelationData; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 12)] + { + /// Authentication arguments used when downloading the package installer if authentication is required. + AuthenticationArguments AuthenticationArguments; + } } [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 11)] @@ -1239,6 +1260,12 @@ namespace Microsoft.Management.Deployment /// Bypasses the Disabled Store Policy Boolean BypassIsStoreClientBlockedPolicyCheck; + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 12)] + { + /// Authentication arguments used when downloading the package installer if authentication is required. + AuthenticationArguments AuthenticationArguments; + } } /// IMPLEMENTATION NOTE: Documentation from AppInstaller::Manifest::Documentation diff --git a/src/Microsoft.Management.Deployment/RepairOptions.cpp b/src/Microsoft.Management.Deployment/RepairOptions.cpp index e321c69483..48b065109e 100644 --- a/src/Microsoft.Management.Deployment/RepairOptions.cpp +++ b/src/Microsoft.Management.Deployment/RepairOptions.cpp @@ -108,5 +108,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation m_force = value; } + winrt::Microsoft::Management::Deployment::AuthenticationArguments RepairOptions::AuthenticationArguments() + { + return m_authenticationArguments; + } + + void RepairOptions::AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value) + { + m_authenticationArguments = value; + } + CoCreatableMicrosoftManagementDeploymentClass(RepairOptions); } diff --git a/src/Microsoft.Management.Deployment/RepairOptions.h b/src/Microsoft.Management.Deployment/RepairOptions.h index 2b083bef6a..909cf9f6f2 100644 --- a/src/Microsoft.Management.Deployment/RepairOptions.h +++ b/src/Microsoft.Management.Deployment/RepairOptions.h @@ -29,7 +29,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation void BypassIsStoreClientBlockedPolicyCheck(bool value); bool Force(); void Force(bool value); - + winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); + void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -42,6 +43,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::wstring m_correlationData = L""; winrt::Microsoft::Management::Deployment::PackageRepairScope m_packageRepairScope = winrt::Microsoft::Management::Deployment::PackageRepairScope::Any; winrt::Microsoft::Management::Deployment::PackageRepairMode m_packageRepairMode = winrt::Microsoft::Management::Deployment::PackageRepairMode::Default; + winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; #endif }; }