blob: 4b99691aa1674db6aa2666ef5036918634d8ca34 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/policy/core/common/policy_merger.h"
#include <array>
#include <map>
#include <set>
#include "base/compiler_specific.h"
#include "build/build_config.h"
#include "components/policy/core/common/features.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/policy/policy_constants.h"
#include "components/strings/grit/components_strings.h"
namespace policy {
namespace {
#if !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA)
constexpr const char* kDictionaryPoliciesToMerge[] = {
#if BUILDFLAG(IS_CHROMEOS)
key::kExtensionSettings, key::kDeviceLoginScreenPowerManagement,
key::kKeyPermissions, key::kPowerManagementIdleSettings,
key::kScreenBrightnessPercent, key::kScreenLockDelays,
#else
key::kExtensionSettings,
#endif // BUILDFLAG(IS_CHROMEOS)
};
#endif // !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID) &&
// !BUILDFLAG(IS_FUCHSIA)
} // namespace
// static
bool PolicyMerger::EntriesCanBeMerged(
const PolicyMap::Entry& entry_1,
const PolicyMap::Entry& entry_2,
const bool is_user_cloud_merging_enabled) {
if (entry_1.value_unsafe()->type() != entry_2.value_unsafe()->type())
return false;
if (entry_1.ignored() || entry_2.ignored() ||
entry_1.source == POLICY_SOURCE_ENTERPRISE_DEFAULT ||
entry_2.source == POLICY_SOURCE_ENTERPRISE_DEFAULT ||
entry_1.level != entry_2.level)
return false;
// If the policies have matching scope and are non-user, they can be merged.
if (entry_1.scope == entry_2.scope && entry_1.scope != POLICY_SCOPE_USER)
return true;
// Merging of user-level GPO policies is not permitted to prevent unexpected
// behavior. If such merging is desired, it will be implemented in a similar
// way as user cloud merging.
if ((entry_1.scope == POLICY_SCOPE_USER &&
entry_1.source == POLICY_SOURCE_PLATFORM) ||
(entry_2.scope == POLICY_SCOPE_USER &&
entry_2.source == POLICY_SOURCE_PLATFORM))
return false;
// On desktop, the user cloud policy potentially comes from a different
// domain than e.g. GPO policy or machine-level cloud policy. Merging a user
// cloud policy with policies from other sources is only permitted if both of
// the following conditions are met:
// 1. The CloudUserPolicyMerge metapolicy is set to True.
// 2. The user is affiliated with the machine-level cloud policy provider.
const bool has_user_cloud_policy = (entry_1.scope == POLICY_SCOPE_USER &&
entry_1.source == POLICY_SOURCE_CLOUD) ||
(entry_2.scope == POLICY_SCOPE_USER &&
entry_2.source == POLICY_SOURCE_CLOUD);
const bool is_user_cloud_condition_satisfied =
!has_user_cloud_policy || is_user_cloud_merging_enabled;
// For the scope condition to be satisfied, either the scopes of the two
// policies should match or the policy override should be enabled. The scope
// check override is only enabled when a user cloud policy is present and user
// cloud merging is enabled -- this allows user cloud policies to merge with
// machine-level policies.
const bool is_scope_overriden =
has_user_cloud_policy && is_user_cloud_merging_enabled;
const bool is_scope_condition_satisfied =
entry_1.scope == entry_2.scope || is_scope_overriden;
return is_user_cloud_condition_satisfied && is_scope_condition_satisfied;
}
PolicyMerger::PolicyMerger() = default;
PolicyMerger::~PolicyMerger() = default;
PolicyListMerger::PolicyListMerger(
base::flat_set<std::string> policies_to_merge)
: policies_to_merge_(std::move(policies_to_merge)) {}
PolicyListMerger::~PolicyListMerger() = default;
PolicyGroupMerger::PolicyGroupMerger() = default;
PolicyGroupMerger::~PolicyGroupMerger() = default;
void PolicyListMerger::Merge(PolicyMap* policies) const {
DCHECK(policies);
for (auto& it : *policies) {
if (CanMerge(it.first, it.second))
DoMerge(&it.second);
}
}
void PolicyListMerger::SetAllowUserCloudPolicyMerging(bool allowed) {
allow_user_cloud_policy_merging_ = allowed;
}
bool PolicyListMerger::CanMerge(const std::string& policy_name,
PolicyMap::Entry& policy) const {
if (policy.source == POLICY_SOURCE_MERGED)
return false;
if (policies_to_merge_.find("*") != policies_to_merge_.end()) {
return policy.HasConflicts() &&
policy.value(base::Value::Type::LIST) != nullptr;
}
if (policies_to_merge_.find(policy_name) == policies_to_merge_.end())
return false;
if (!policy.value(base::Value::Type::LIST)) {
policy.AddMessage(PolicyMap::MessageType::kError,
IDS_POLICY_LIST_MERGING_WRONG_POLICY_TYPE_SPECIFIED);
return false;
}
return policy.HasConflicts();
}
bool PolicyListMerger::AllowUserCloudPolicyMerging() const {
return allow_user_cloud_policy_merging_;
}
void PolicyListMerger::DoMerge(PolicyMap::Entry* policy) const {
std::vector<const base::Value*> merged_values;
auto compare_value_ptr = [](const base::Value* a, const base::Value* b) {
return *a < *b;
};
std::set<const base::Value*, decltype(compare_value_ptr)> duplicates(
compare_value_ptr);
bool value_changed = false;
for (const base::Value& val :
policy->value(base::Value::Type::LIST)->GetList()) {
if (duplicates.find(&val) != duplicates.end())
continue;
duplicates.insert(&val);
merged_values.push_back(&val);
}
// Concatenates the values from accepted conflicting sources to the policy
// value while avoiding duplicates.
for (const auto& it : policy->conflicts) {
if (!PolicyMerger::EntriesCanBeMerged(it.entry(), *policy,
AllowUserCloudPolicyMerging())) {
continue;
}
for (const base::Value& val :
it.entry().value(base::Value::Type::LIST)->GetList()) {
if (duplicates.find(&val) != duplicates.end())
continue;
duplicates.insert(&val);
merged_values.push_back(&val);
}
value_changed = true;
}
auto new_conflict = policy->DeepCopy();
if (value_changed) {
base::Value::List new_value;
for (const base::Value* it : merged_values)
new_value.Append(it->Clone());
policy->set_value(base::Value(std::move(new_value)));
}
policy->ClearConflicts();
policy->AddConflictingPolicy(std::move(new_conflict));
policy->source = POLICY_SOURCE_MERGED;
}
PolicyDictionaryMerger::PolicyDictionaryMerger(
base::flat_set<std::string> policies_to_merge)
#if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_FUCHSIA)
: policies_to_merge_(std::move(policies_to_merge)){}
#else
: policies_to_merge_(std::move(policies_to_merge)),
allowed_policies_(std::begin(kDictionaryPoliciesToMerge),
std::end(kDictionaryPoliciesToMerge)) {
}
#endif
PolicyDictionaryMerger::~PolicyDictionaryMerger() = default;
void PolicyDictionaryMerger::Merge(PolicyMap* policies) const {
DCHECK(policies);
for (auto& it : *policies) {
if (CanMerge(it.first, it.second))
DoMerge(&it.second, *policies);
}
}
void PolicyDictionaryMerger::SetAllowedPoliciesForTesting(
base::flat_set<std::string> allowed_policies) {
allowed_policies_ = std::move(allowed_policies);
}
void PolicyDictionaryMerger::SetAllowUserCloudPolicyMerging(bool allowed) {
allow_user_cloud_policy_merging_ = allowed;
}
bool PolicyDictionaryMerger::CanMerge(const std::string& policy_name,
PolicyMap::Entry& policy) const {
if (policy.source == POLICY_SOURCE_MERGED)
return false;
const bool allowed_to_merge =
allowed_policies_.find(policy_name) != allowed_policies_.end();
if (policies_to_merge_.find("*") != policies_to_merge_.end()) {
return allowed_to_merge && policy.HasConflicts() &&
policy.value(base::Value::Type::DICT);
}
if (policies_to_merge_.find(policy_name) == policies_to_merge_.end())
return false;
if (!allowed_to_merge) {
policy.AddMessage(PolicyMap::MessageType::kError,
IDS_POLICY_DICTIONARY_MERGING_POLICY_NOT_ALLOWED);
return false;
}
if (!policy.value(base::Value::Type::DICT)) {
policy.AddMessage(
PolicyMap::MessageType::kError,
IDS_POLICY_DICTIONARY_MERGING_WRONG_POLICY_TYPE_SPECIFIED);
return false;
}
return policy.HasConflicts();
}
bool PolicyDictionaryMerger::AllowUserCloudPolicyMerging() const {
return allow_user_cloud_policy_merging_;
}
void PolicyDictionaryMerger::DoMerge(PolicyMap::Entry* policy,
const PolicyMap& policy_map) const {
// Keep priority sorted list of potential merge targets.
std::vector<const PolicyMap::Entry*> policies;
policies.push_back(policy);
for (const auto& it : policy->conflicts)
policies.push_back(&it.entry());
std::sort(
policies.begin(), policies.end(),
[&policy_map](const PolicyMap::Entry* a, const PolicyMap::Entry* b) {
return policy_map.EntryHasHigherPriority(*b, *a);
});
base::Value::Dict merged_dictionary;
bool value_changed = false;
// Merges all the keys from the policies from different sources.
for (const auto* it : policies) {
if (it != policy && !PolicyMerger::EntriesCanBeMerged(
*it, *policy, AllowUserCloudPolicyMerging()))
continue;
const base::Value::Dict* dict =
it->value(base::Value::Type::DICT)->GetIfDict();
DCHECK(dict);
for (auto pair : *dict) {
const auto& key = pair.first;
const auto& val = pair.second;
merged_dictionary.Set(key, val.Clone());
}
value_changed |= it != policy;
}
auto new_conflict = policy->DeepCopy();
if (value_changed)
policy->set_value(base::Value(std::move(merged_dictionary)));
policy->ClearConflicts();
policy->AddConflictingPolicy(std::move(new_conflict));
policy->source = POLICY_SOURCE_MERGED;
}
void PolicyGroupMerger::Merge(PolicyMap* policies) const {
for (size_t i = 0; i < kPolicyAtomicGroupMappingsLength; ++i) {
const AtomicGroup& group = UNSAFE_TODO(kPolicyAtomicGroupMappings[i]);
bool use_highest_set_priority = false;
// Defaults to the lowest priority.
PolicyMap::Entry highest_set_priority;
// Find the policy with the highest priority that is both in |policies| and
// |group.policies|, an array ending with a nullptr.
for (const char* const* policy_name = group.policies; *policy_name;
UNSAFE_TODO(++policy_name)) {
const auto* policy = policies->Get(*policy_name);
if (!policy)
continue;
use_highest_set_priority = true;
if (!policies->EntryHasHigherPriority(*policy, highest_set_priority))
continue;
// Do not set POLICY_SOURCE_MERGED as the highest acceptable source
// because it is a computed source. In case of an already merged policy,
// the highest acceptable source must be the highest of the ones used to
// compute the merged value.
if (policy->source != POLICY_SOURCE_MERGED) {
highest_set_priority = policy->DeepCopy();
} else {
for (const auto& conflict : policy->conflicts) {
if (policies->EntryHasHigherPriority(conflict.entry(),
highest_set_priority) &&
conflict.entry().source > highest_set_priority.source) {
highest_set_priority = conflict.entry().DeepCopy();
}
}
}
}
if (!use_highest_set_priority)
continue;
// Ignore the policies from |group.policies|, an array ending with a
// nullptr, that do not share the same source as the one with the highest
// priority.
for (const char* const* policy_name = group.policies; *policy_name;
UNSAFE_TODO(++policy_name)) {
auto* policy = policies->GetMutable(*policy_name);
if (!policy)
continue;
if (policy->source < highest_set_priority.source)
policy->SetIgnoredByPolicyAtomicGroup();
}
}
}
} // namespace policy