blob: 7722ee14945396da46c11c7b08d9b8a00de75a94 [file] [log] [blame]
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// 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_loader_win.h"
#include <lm.h> // For NetGetJoinInformation
// <security.h> needs this.
#define SECURITY_WIN32 1
#include <security.h> // For GetUserNameEx()
#include <stddef.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/scoped_native_library.h"
#include "base/sequenced_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "base/win/shlwapi.h" // For PathIsUNC()
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "components/policy/core/common/policy_bundle.h"
#include "components/policy/core/common/policy_load_status.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_namespace.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/core/common/registry_dict.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/policy_constants.h"
namespace policy {
namespace {
const char kKeyMandatory[] = "policy";
const char kKeyRecommended[] = "recommended";
const char kKeyThirdParty[] = "3rdparty";
// The web store url that is the only trusted source for extensions.
const char kExpectedWebStoreUrl[] =
";https://clients2.google.com/service/update2/crx";
// String to be prepended to each blocked entry.
const char kBlockedExtensionPrefix[] = "[BLOCKED]";
// List of policies that are considered only if the user is part of a AD domain.
// Please document any new additions in policy_templates.json!
const char* kInsecurePolicies[] = {
key::kChromeCleanupEnabled,
key::kChromeCleanupReportingEnabled,
key::kCloudPolicyOverridesMachinePolicy,
key::kDefaultSearchProviderEnabled,
key::kHomepageIsNewTabPage,
key::kHomepageLocation,
key::kMetricsReportingEnabled,
key::kNewTabPageLocation,
key::kPasswordProtectionChangePasswordURL,
key::kPasswordProtectionLoginURLs,
key::kRestoreOnStartup,
key::kRestoreOnStartupURLs,
key::kSafeBrowsingForTrustedSourcesEnabled,
key::kSafeBrowsingEnabled,
key::kSafeBrowsingWhitelistDomains,
};
// The list of possible errors that can occur while collecting information about
// the current enterprise environment.
// This enum is used to define the buckets for an enumerated UMA histogram.
// Hence,
// (a) existing enumerated constants should never be deleted or reordered, and
// (b) new constants should only be appended at the end of the enumeration.
enum DomainCheckErrors {
// The check error below is no longer possible.
DEPRECATED_DOMAIN_CHECK_ERROR_GET_JOIN_INFO = 0,
DOMAIN_CHECK_ERROR_DS_BIND = 1,
DOMAIN_CHECK_ERROR_SIZE, // Not a DomainCheckError. Must be last.
};
// Encapculates logic to determine if enterprise policies should be honored.
// This is used in various places below.
bool ShouldHonorPolicies() {
bool is_enterprise_version =
base::win::OSInfo::GetInstance()->version_type() != base::win::SUITE_HOME;
return base::win::IsEnrolledToDomain() ||
(base::win::IsDeviceRegisteredWithManagement() &&
is_enterprise_version);
}
// Verifies that untrusted policies contain only safe values. Modifies the
// |policy| in place.
void FilterUntrustedPolicy(PolicyMap* policy) {
if (ShouldHonorPolicies())
return;
int invalid_policies = 0;
const PolicyMap::Entry* map_entry =
policy->Get(key::kExtensionInstallForcelist);
if (map_entry && map_entry->value) {
const base::ListValue* policy_list_value = nullptr;
if (!map_entry->value->GetAsList(&policy_list_value))
return;
std::unique_ptr<base::ListValue> filtered_values(new base::ListValue);
for (const auto& list_entry : *policy_list_value) {
std::string entry;
if (!list_entry.GetAsString(&entry))
continue;
size_t pos = entry.find(';');
if (pos == std::string::npos)
continue;
// Only allow custom update urls in enterprise environments.
if (!base::LowerCaseEqualsASCII(entry.substr(pos),
kExpectedWebStoreUrl)) {
entry = kBlockedExtensionPrefix + entry;
invalid_policies++;
}
filtered_values->AppendString(entry);
}
if (invalid_policies) {
PolicyMap::Entry filtered_entry = map_entry->DeepCopy();
filtered_entry.value = std::move(filtered_values);
policy->Set(key::kExtensionInstallForcelist, std::move(filtered_entry));
const PolicyDetails* details =
GetChromePolicyDetails(key::kExtensionInstallForcelist);
base::UmaHistogramSparse("EnterpriseCheck.InvalidPolicies", details->id);
}
}
for (size_t i = 0; i < base::size(kInsecurePolicies); ++i) {
if (policy->Get(kInsecurePolicies[i])) {
// TODO(pastarmovj): Surface this issue in the about:policy page.
policy->Erase(kInsecurePolicies[i]);
invalid_policies++;
const PolicyDetails* details =
GetChromePolicyDetails(kInsecurePolicies[i]);
base::UmaHistogramSparse("EnterpriseCheck.InvalidPolicies", details->id);
}
}
UMA_HISTOGRAM_COUNTS_1M("EnterpriseCheck.InvalidPoliciesDetected",
invalid_policies);
}
// Parses |gpo_dict| according to |schema| and writes the resulting policy
// settings to |policy| for the given |scope| and |level|.
void ParsePolicy(const RegistryDict* gpo_dict,
PolicyLevel level,
PolicyScope scope,
const Schema& schema,
PolicyMap* policy) {
if (!gpo_dict)
return;
std::unique_ptr<base::Value> policy_value(gpo_dict->ConvertToJSON(schema));
const base::DictionaryValue* policy_dict = nullptr;
if (!policy_value->GetAsDictionary(&policy_dict) || !policy_dict) {
LOG(WARNING) << "Root policy object is not a dictionary!";
return;
}
policy->LoadFrom(policy_dict, level, scope, POLICY_SOURCE_PLATFORM);
}
// Returns a name, using the |get_name| callback, which may refuse the call if
// the name is longer than _MAX_PATH. So this helper function takes care of the
// retry with the required size.
bool GetName(const base::Callback<BOOL(LPWSTR, LPDWORD)>& get_name,
base::string16* name) {
DCHECK(name);
DWORD size = _MAX_PATH;
if (!get_name.Run(base::WriteInto(name, size), &size)) {
if (::GetLastError() != ERROR_MORE_DATA)
return false;
// Try again with the required size. This time it must work, the size should
// not have changed in between the two calls.
if (!get_name.Run(base::WriteInto(name, size), &size))
return false;
}
return true;
}
// To convert the weird BOOLEAN return value type of ::GetUserNameEx().
BOOL GetUserNameExBool(EXTENDED_NAME_FORMAT format, LPWSTR name, PULONG size) {
// ::GetUserNameEx is documented to return a nonzero value on success.
return ::GetUserNameEx(format, name, size) != 0;
}
// Make sure to use the real NetGetJoinInformation, otherwise fallback to the
// linked one.
bool IsDomainJoined() {
base::ScopedClosureRunner free_library;
decltype(&::NetGetJoinInformation) net_get_join_information_function =
&::NetGetJoinInformation;
decltype(&::NetApiBufferFree) net_api_buffer_free_function =
&::NetApiBufferFree;
bool got_function_addresses = false;
// Use an absolute path to load the DLL to avoid DLL preloading attacks.
base::FilePath path;
if (base::PathService::Get(base::DIR_SYSTEM, &path)) {
HINSTANCE net_api_library = ::LoadLibraryEx(
path.Append(FILE_PATH_LITERAL("netapi32.dll")).value().c_str(), nullptr,
LOAD_WITH_ALTERED_SEARCH_PATH);
if (net_api_library) {
free_library.ReplaceClosure(
base::BindOnce(base::IgnoreResult(&::FreeLibrary), net_api_library));
net_get_join_information_function =
reinterpret_cast<decltype(&::NetGetJoinInformation)>(
::GetProcAddress(net_api_library, "NetGetJoinInformation"));
net_api_buffer_free_function =
reinterpret_cast<decltype(&::NetApiBufferFree)>(
::GetProcAddress(net_api_library, "NetApiBufferFree"));
if (net_get_join_information_function && net_api_buffer_free_function) {
got_function_addresses = true;
} else {
net_get_join_information_function = &::NetGetJoinInformation;
net_api_buffer_free_function = &::NetApiBufferFree;
}
}
}
base::UmaHistogramBoolean("EnterpriseCheck.NetGetJoinInformationAddress",
got_function_addresses);
LPWSTR buffer = nullptr;
NETSETUP_JOIN_STATUS buffer_type = NetSetupUnknownStatus;
bool is_joined = net_get_join_information_function(
nullptr, &buffer, &buffer_type) == NERR_Success &&
buffer_type == NetSetupDomainName;
if (buffer)
net_api_buffer_free_function(buffer);
return is_joined;
}
// Collects stats about the enterprise environment that can be used to decide
// how to parse the existing policy information.
void CollectEnterpriseUMAs() {
// Collect statistics about the windows suite.
UMA_HISTOGRAM_ENUMERATION("EnterpriseCheck.OSType",
base::win::OSInfo::GetInstance()->version_type(),
base::win::SUITE_LAST);
base::UmaHistogramBoolean("EnterpriseCheck.IsDomainJoined", IsDomainJoined());
base::UmaHistogramBoolean("EnterpriseCheck.InDomain",
base::win::IsEnrolledToDomain());
base::UmaHistogramBoolean("EnterpriseCheck.IsManaged",
base::win::IsDeviceRegisteredWithManagement());
base::UmaHistogramBoolean("EnterpriseCheck.IsEnterpriseUser",
base::win::IsEnterpriseManaged());
base::string16 machine_name;
if (GetName(base::Bind(&::GetComputerNameEx, ::ComputerNameDnsHostname),
&machine_name)) {
base::string16 user_name;
if (GetName(base::Bind(&GetUserNameExBool, ::NameSamCompatible),
&user_name)) {
// A local user has the machine name in its sam compatible name, e.g.,
// 'MACHINE_NAME\username', otherwise it is perfixed with the domain name
// as opposed to the machine, e.g., 'COMPANY\username'.
base::UmaHistogramBoolean(
"EnterpriseCheck.IsLocalUser",
base::StartsWith(user_name, machine_name,
base::CompareCase::INSENSITIVE_ASCII) &&
user_name[machine_name.size()] == L'\\');
}
base::string16 full_machine_name;
if (GetName(
base::Bind(&::GetComputerNameEx, ::ComputerNameDnsFullyQualified),
&full_machine_name)) {
// ComputerNameDnsFullyQualified is the same as the
// ComputerNameDnsHostname when not domain joined, otherwise it has a
// suffix.
base::UmaHistogramBoolean(
"EnterpriseCheck.IsLocalMachine",
base::EqualsCaseInsensitiveASCII(machine_name, full_machine_name));
}
}
}
} // namespace
PolicyLoaderWin::PolicyLoaderWin(
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::string16& chrome_policy_key)
: AsyncPolicyLoader(task_runner),
is_initialized_(false),
chrome_policy_key_(chrome_policy_key),
user_policy_changed_event_(
base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED),
machine_policy_changed_event_(
base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED),
user_policy_watcher_failed_(false),
machine_policy_watcher_failed_(false) {
if (!::RegisterGPNotification(user_policy_changed_event_.handle(), false)) {
DPLOG(WARNING) << "Failed to register user group policy notification";
user_policy_watcher_failed_ = true;
}
if (!::RegisterGPNotification(machine_policy_changed_event_.handle(), true)) {
DPLOG(WARNING) << "Failed to register machine group policy notification.";
machine_policy_watcher_failed_ = true;
}
}
PolicyLoaderWin::~PolicyLoaderWin() {
if (!user_policy_watcher_failed_) {
::UnregisterGPNotification(user_policy_changed_event_.handle());
user_policy_watcher_.StopWatching();
}
if (!machine_policy_watcher_failed_) {
::UnregisterGPNotification(machine_policy_changed_event_.handle());
machine_policy_watcher_.StopWatching();
}
}
// static
std::unique_ptr<PolicyLoaderWin> PolicyLoaderWin::Create(
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::string16& chrome_policy_key) {
return base::WrapUnique(new PolicyLoaderWin(task_runner, chrome_policy_key));
}
void PolicyLoaderWin::InitOnBackgroundThread() {
is_initialized_ = true;
SetupWatches();
CollectEnterpriseUMAs();
}
std::unique_ptr<PolicyBundle> PolicyLoaderWin::Load() {
// Reset the watches BEFORE reading the individual policies to avoid
// missing a change notification.
if (is_initialized_)
SetupWatches();
// Policy scope and corresponding hive.
static const struct {
PolicyScope scope;
HKEY hive;
} kScopes[] = {
{POLICY_SCOPE_MACHINE, HKEY_LOCAL_MACHINE},
{POLICY_SCOPE_USER, HKEY_CURRENT_USER},
};
// Load policy data for the different scopes/levels and merge them.
std::unique_ptr<PolicyBundle> bundle(new PolicyBundle());
PolicyMap* chrome_policy =
&bundle->Get(PolicyNamespace(POLICY_DOMAIN_CHROME, std::string()));
for (size_t i = 0; i < base::size(kScopes); ++i) {
PolicyScope scope = kScopes[i].scope;
PolicyLoadStatusUmaReporter status;
RegistryDict gpo_dict;
gpo_dict.ReadRegistry(kScopes[i].hive, chrome_policy_key_);
// Remove special-cased entries from the GPO dictionary.
std::unique_ptr<RegistryDict> recommended_dict(
gpo_dict.RemoveKey(kKeyRecommended));
std::unique_ptr<RegistryDict> third_party_dict(
gpo_dict.RemoveKey(kKeyThirdParty));
// Load Chrome policy.
LoadChromePolicy(&gpo_dict, POLICY_LEVEL_MANDATORY, scope, chrome_policy);
LoadChromePolicy(recommended_dict.get(), POLICY_LEVEL_RECOMMENDED, scope,
chrome_policy);
// Load 3rd-party policy.
if (third_party_dict)
Load3rdPartyPolicy(third_party_dict.get(), scope, bundle.get());
}
return bundle;
}
void PolicyLoaderWin::LoadChromePolicy(const RegistryDict* gpo_dict,
PolicyLevel level,
PolicyScope scope,
PolicyMap* chrome_policy_map) {
PolicyMap policy;
const Schema* chrome_schema =
schema_map()->GetSchema(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
ParsePolicy(gpo_dict, level, scope, *chrome_schema, &policy);
FilterUntrustedPolicy(&policy);
chrome_policy_map->MergeFrom(policy);
}
void PolicyLoaderWin::Load3rdPartyPolicy(const RegistryDict* gpo_dict,
PolicyScope scope,
PolicyBundle* bundle) {
// Map of known 3rd party policy domain name to their enum values.
static const struct {
const char* name;
PolicyDomain domain;
} k3rdPartyDomains[] = {
{"extensions", POLICY_DOMAIN_EXTENSIONS},
};
// Policy level and corresponding path.
static const struct {
PolicyLevel level;
const char* path;
} kLevels[] = {
{POLICY_LEVEL_MANDATORY, kKeyMandatory},
{POLICY_LEVEL_RECOMMENDED, kKeyRecommended},
};
for (size_t i = 0; i < base::size(k3rdPartyDomains); i++) {
const char* name = k3rdPartyDomains[i].name;
const PolicyDomain domain = k3rdPartyDomains[i].domain;
const RegistryDict* domain_dict = gpo_dict->GetKey(name);
if (!domain_dict)
continue;
for (RegistryDict::KeyMap::const_iterator component(
domain_dict->keys().begin());
component != domain_dict->keys().end(); ++component) {
const PolicyNamespace policy_namespace(domain, component->first);
const Schema* schema_from_map = schema_map()->GetSchema(policy_namespace);
if (!schema_from_map) {
// This extension isn't installed or doesn't support policies.
continue;
}
Schema schema = *schema_from_map;
// Parse policy.
for (size_t j = 0; j < base::size(kLevels); j++) {
const RegistryDict* policy_dict =
component->second->GetKey(kLevels[j].path);
if (!policy_dict)
continue;
PolicyMap policy;
ParsePolicy(policy_dict, kLevels[j].level, scope, schema, &policy);
bundle->Get(policy_namespace).MergeFrom(policy);
}
}
}
}
void PolicyLoaderWin::SetupWatches() {
DCHECK(is_initialized_);
if (!user_policy_watcher_failed_ &&
!user_policy_watcher_.GetWatchedObject() &&
!user_policy_watcher_.StartWatchingOnce(
user_policy_changed_event_.handle(), this)) {
DLOG(WARNING) << "Failed to start watch for user policy change event";
user_policy_watcher_failed_ = true;
}
if (!machine_policy_watcher_failed_ &&
!machine_policy_watcher_.GetWatchedObject() &&
!machine_policy_watcher_.StartWatchingOnce(
machine_policy_changed_event_.handle(), this)) {
DLOG(WARNING) << "Failed to start watch for machine policy change event";
machine_policy_watcher_failed_ = true;
}
}
void PolicyLoaderWin::OnObjectSignaled(HANDLE object) {
DCHECK(object == user_policy_changed_event_.handle() ||
object == machine_policy_changed_event_.handle())
<< "unexpected object signaled policy reload, obj = " << std::showbase
<< std::hex << object;
Reload(false);
}
} // namespace policy