Skip to content

Commit

Permalink
Dynamic runtime configuration support (microsoft#4355)
Browse files Browse the repository at this point in the history
Credit to @JohnMcPMS for initial prototyping.

For reference:
`DynamicFactory` holds the same integrity level remote factory
`DynamicFactory` creates `DynamicSetProcessor`, giving it a copy of the
same integrity level remote factory
`DynamicSetProcessor` creates more remote factories and remote set
processors at other integrity levels as needed
`DynamicSetProcessor` uses the appropriate remote set processor to
create the unit processor

Changes:
Added logic to separate out units with high integrity levels and
serializing those units into a yaml string. This yaml string will be
passed to `CreateOutOfProcessFactory()` along with the json string
containing the "path" as the limitation set in the configuration remote
server and processors.

Tests:
I only added one test for verifying serialization and manually verified
the json output for `path`. In the interest of time, I created this PR
to get out the feature and will continue to work on adding more thorough
E2E testing after everything is fully integrated.
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4355)

---------

Co-authored-by: John McPherson <[email protected]>
  • Loading branch information
2 people authored and yao-msft committed Apr 9, 2024
1 parent 26a4486 commit 3507cd3
Show file tree
Hide file tree
Showing 39 changed files with 1,156 additions and 304 deletions.
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ diskfull
DISMAPI
dnld
Dobbeleer
DONOT
dsc
DUPLICATEALIAS
dustojnikhummer
Expand Down Expand Up @@ -465,6 +466,7 @@ uninstalls
Unk
unknwn
Unknwnbase
UNMARSHALING
unparsable
unvirtualized
UParse
Expand Down
11 changes: 11 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,14 @@ You can enable the feature as shown below.
"configuration03": true
},
```

### configureSelfElevate

This feature enables configure commands to request elevation as needed.
Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context.

```json
"experimentalFeatures": {
"configureSelfElevate": true
},
```
5 changes: 5 additions & 0 deletions schemas/JSON/settings/settings.schema.0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@
"description": "Enable support for configuration",
"type": "boolean",
"default": false
},
"configureSelfElevate": {
"description": "Enable configure commands request elevation as needed",
"type": "boolean",
"default": false
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@
<ClCompile Include="Commands\TestCommand.cpp" />
<ClCompile Include="ConfigurationCommon.cpp" />
<ClCompile Include="ConfigurationContext.cpp" />
<ClCompile Include="ConfigurationDynamicRuntimeFactory.cpp" />
<ClCompile Include="ConfigurationSetProcessorFactoryRemoting.cpp" />
<ClCompile Include="ConfigurationWingetDscModuleUnitValidation.cpp" />
<ClCompile Include="ContextOrchestrator.cpp" />
Expand Down
13 changes: 11 additions & 2 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@
<ClInclude Include="Workflows\RepairFlow.h">
<Filter>Workflows</Filter>
</ClInclude>
<ClInclude Include="ConfigurationWingetDscModuleUnitValidation.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
Expand Down Expand Up @@ -456,13 +459,19 @@
</ClCompile>
<ClCompile Include="Commands\ErrorCommand.cpp">
<Filter>Commands</Filter>
</ClCompile>
</ClCompile>
<ClCompile Include="Commands\RepairCommand.cpp">
<Filter>Commands</Filter>
</ClCompile>
</ClCompile>
<ClCompile Include="Workflows\RepairFlow.cpp">
<Filter>Workflows</Filter>
</ClCompile>
<ClCompile Include="ConfigurationWingetDscModuleUnitValidation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ConfigurationDynamicRuntimeFactory.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
Expand Down
245 changes: 245 additions & 0 deletions src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include "Public/ConfigurationSetProcessorFactoryRemoting.h"
#include <AppInstallerStrings.h>
#include <winget/ILifetimeWatcher.h>
#include <winget/Security.h>

using namespace winrt::Windows::Foundation;
using namespace winrt::Microsoft::Management::Configuration;
using namespace winrt::Windows::Storage;

namespace AppInstaller::CLI::ConfigurationRemoting
{
namespace anonymous
{
struct DynamicProcessorInfo
{
IConfigurationSetProcessorFactory Factory;
IConfigurationSetProcessor Processor;
};

struct DynamicSetProcessor : winrt::implements<DynamicSetProcessor, IConfigurationSetProcessor>
{
using ProcessorMap = std::map<Security::IntegrityLevel, DynamicProcessorInfo>;

DynamicSetProcessor(IConfigurationSetProcessorFactory defaultRemoteFactory, IConfigurationSetProcessor defaultRemoteSetProcessor, const ConfigurationSet& configurationSet) : m_configurationSet(configurationSet)
{
m_currentIntegrityLevel = Security::GetEffectiveIntegrityLevel();
m_setProcessors.emplace(m_currentIntegrityLevel, DynamicProcessorInfo{ defaultRemoteFactory, defaultRemoteSetProcessor });
}

IConfigurationUnitProcessorDetails GetUnitProcessorDetails(const ConfigurationUnit& unit, ConfigurationUnitDetailFlags detailFlags)
{
// Always get processor details from the current integrity level
return m_setProcessors[m_currentIntegrityLevel].Processor.GetUnitProcessorDetails(unit, detailFlags);
}

// Creates a configuration unit processor for the given unit.
IConfigurationUnitProcessor CreateUnitProcessor(const ConfigurationUnit& unit)
{
// Determine and create set processors for all required integrity levels.
// Doing this here avoids creating them if the only call is going to be for details (ex. `configure show`)
std::call_once(m_createUnitSetProcessorsOnce,
[&]()
{
for (const auto& existingUnit : m_configurationSet.Units())
{
Security::IntegrityLevel requiredIntegrityLevel = GetIntegrityLevelForUnit(existingUnit);

if (m_setProcessors.find(requiredIntegrityLevel) == m_setProcessors.end())
{
CreateSetProcessorForIntegrityLevel(requiredIntegrityLevel);
}
}
});

// Create set and unit processor for current unit.
Security::IntegrityLevel requiredIntegrityLevel = GetIntegrityLevelForUnit(unit);

auto itr = m_setProcessors.find(requiredIntegrityLevel);
if (itr == m_setProcessors.end())
{
itr = CreateSetProcessorForIntegrityLevel(requiredIntegrityLevel);
}

return itr->second.Processor.CreateUnitProcessor(unit);
}

private:
// Converts the string representation of SecurityContext to the integrity level
Security::IntegrityLevel SecurityContextToIntegrityLevel(winrt::hstring securityContext)
{
std::wstring securityContextLower = Utility::ToLower(securityContext);

if (securityContextLower == L"elevated")
{
return Security::IntegrityLevel::High;
}
else if (securityContextLower == L"restricted")
{
// Not supporting elevated callers downgrading at the moment.
THROW_WIN32(ERROR_NOT_SUPPORTED);

// Technically this means the default level of the user token, so if UAC is disabled it would be the only integrity level (aka current).
//return Security::IntegrityLevel::Medium;
}
else if (securityContextLower == L"current")
{
return m_currentIntegrityLevel;
}

THROW_WIN32(ERROR_NOT_SUPPORTED);
}

// Gets the integrity level that the given unit should be run at
Security::IntegrityLevel GetIntegrityLevelForUnit(const ConfigurationUnit& unit)
{
// Support for 0.2 schema via metadata value
// TODO: Support case insensitive lookup by iteration
auto unitMetadata = unit.Metadata();
auto securityContext = unitMetadata.TryLookup(L"SecurityContext");
if (securityContext)
{
auto securityContextProperty = securityContext.try_as<IPropertyValue>();
if (securityContextProperty && securityContextProperty.Type() == PropertyType::String)
{
return SecurityContextToIntegrityLevel(securityContextProperty.GetString());
}
}

// TODO: Support for 0.3 schema will require a group processor wrapper

return m_currentIntegrityLevel;
}

// Serializes the set properties to be sent to the remote server
std::string SerializeSetProperties()
{
Json::Value json{ Json::ValueType::objectValue };
json["path"] = winrt::to_string(m_configurationSet.Path());
Json::StreamWriterBuilder writerBuilder;
writerBuilder.settings_["indentation"] = "\t";
return Json::writeString(writerBuilder, json);
}

/// <summary>
/// Creates a separate configuration set containing high integrity units and returns the serialized string value.
/// </summary>
/// <returns>Serialized string value.</returns>
std::string SerializeHighIntegrityLevelSet()
{
ConfigurationSet highIntegritySet;

// TODO: Currently we only support schema version 0.2 for handling elevated integrity levels.
highIntegritySet.SchemaVersion(L"0.2");

std::vector<ConfigurationUnit> highIntegrityUnits;
auto units = m_configurationSet.Units();

for (auto unit : units)
{
if (unit.IsActive() && GetIntegrityLevelForUnit(unit) == Security::IntegrityLevel::High)
{
highIntegrityUnits.emplace_back(unit);
}
}

highIntegritySet.Units(std::move(highIntegrityUnits));

// Serialize high integrity set and return output string.
Streams::InMemoryRandomAccessStream memoryStream;
highIntegritySet.Serialize(memoryStream);

Streams::DataReader reader(memoryStream.GetInputStreamAt(0));
reader.UnicodeEncoding(Streams::UnicodeEncoding::Utf8);
reader.LoadAsync((uint32_t)memoryStream.Size());

winrt::hstring result;
uint32_t bytesToRead = reader.UnconsumedBufferLength();

if (bytesToRead > 0)
{
result = reader.ReadString(bytesToRead);
}

return winrt::to_string(result);
}

ProcessorMap::iterator CreateSetProcessorForIntegrityLevel(Security::IntegrityLevel integrityLevel)
{
IConfigurationSetProcessorFactory factory;

// If we got here, the only option is that the current integrity level is not High.
if (integrityLevel == Security::IntegrityLevel::High)
{
factory = CreateOutOfProcessFactory(true, SerializeSetProperties(), SerializeHighIntegrityLevelSet());
}
else
{
THROW_WIN32(ERROR_NOT_SUPPORTED);
}

return m_setProcessors.emplace(integrityLevel, DynamicProcessorInfo{ factory, factory.CreateSetProcessor(m_configurationSet) }).first;
}

Security::IntegrityLevel m_currentIntegrityLevel;
ProcessorMap m_setProcessors;
ConfigurationSet m_configurationSet;
std::once_flag m_createUnitSetProcessorsOnce;
};

// This is implemented completely in the packaged context for now, if we want to make it more configurable, we will probably want to move it to configuration and
// have this implementation leverage that one with an event handler for the packaged specifics.
// TODO: Add SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties and pass values along to sets on creation
// In turn, any properties must only be set via the command line (or eventual UI requests to the user).
struct DynamicFactory : winrt::implements<DynamicFactory, IConfigurationSetProcessorFactory, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase
{
DynamicFactory()
{
m_defaultRemoteFactory = CreateOutOfProcessFactory();
}

IConfigurationSetProcessor CreateSetProcessor(const ConfigurationSet& configurationSet)
{
return winrt::make<DynamicSetProcessor>(m_defaultRemoteFactory, m_defaultRemoteFactory.CreateSetProcessor(configurationSet), configurationSet);
}

winrt::event_token Diagnostics(const EventHandler<IDiagnosticInformation>&)
{
// TODO: If we want diagnostics here, see ConfigurationProcessor for how to integrate nicely with the infrastructure.
// Best solution is probably to create a base class that both can leverage to handle it cleanly.
return {};
}

void Diagnostics(const winrt::event_token&) noexcept
{
}

DiagnosticLevel MinimumLevel()
{
return m_minimumLevel;
}

void MinimumLevel(DiagnosticLevel value)
{
m_minimumLevel = value;
}

HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher)
{
return WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher);
}

private:
IConfigurationSetProcessorFactory m_defaultRemoteFactory;
DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational;
};
}

winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateDynamicRuntimeFactory()
{
return winrt::make<anonymous::DynamicFactory>();
}
}
Loading

0 comments on commit 3507cd3

Please sign in to comment.