Index: chrome/browser/policy/policy_loader_win.cc |
diff --git a/chrome/browser/policy/policy_loader_win.cc b/chrome/browser/policy/policy_loader_win.cc |
index b527308a3b73dde7d3dda7a4d002808b615fa361..9af680970afec4fa99ea02f551dcb715ecb06395 100644 |
--- a/chrome/browser/policy/policy_loader_win.cc |
+++ b/chrome/browser/policy/policy_loader_win.cc |
@@ -5,25 +5,34 @@ |
#include "chrome/browser/policy/policy_loader_win.h" |
#include <string> |
+#include <vector> |
-#include <string.h> |
+#include <rpc.h> // For struct GUID |
+#include <shlwapi.h> // For PathIsUNC() |
Joao da Silva
2013/04/10 12:32:12
Suggestion: I find vertically aligned comments eas
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
With my nitpick hat on, I'd agree :) But being hon
|
+#include <windows.h> |
-#include <userenv.h> |
- |
-// userenv.dll is required for RegisterGPNotification(). |
+// shlwapi.dll is required for PathIsUNC(). |
+#pragma comment(lib, "shlwapi.lib") |
+// userenv.dll is required for various GPO functions. |
#pragma comment(lib, "userenv.lib") |
#include "base/basictypes.h" |
+#include "base/file_util.h" |
+#include "base/files/file_path.h" |
#include "base/json/json_reader.h" |
+#include "base/lazy_instance.h" |
#include "base/logging.h" |
-#include "base/memory/scoped_ptr.h" |
+#include "base/scoped_native_library.h" |
+#include "base/stl_util.h" |
#include "base/string16.h" |
+#include "base/string_util.h" |
#include "base/strings/string_number_conversions.h" |
+#include "base/sys_byteorder.h" |
#include "base/utf_string_conversions.h" |
-#include "base/values.h" |
#include "base/win/registry.h" |
#include "chrome/browser/policy/policy_bundle.h" |
#include "chrome/browser/policy/policy_map.h" |
+#include "chrome/browser/policy/preg_parser_win.h" |
#include "chrome/common/json_schema/json_schema_constants.h" |
#include "policy/policy_constants.h" |
@@ -32,70 +41,183 @@ namespace schema = json_schema_constants; |
using base::win::RegKey; |
using base::win::RegistryKeyIterator; |
using base::win::RegistryValueIterator; |
-using namespace policy::registry_constants; |
namespace policy { |
-namespace registry_constants { |
- const wchar_t kPathSep[] = L"\\"; |
- const wchar_t kThirdParty[] = L"3rdparty"; |
- const wchar_t kMandatory[] = L"policy"; |
- const wchar_t kRecommended[] = L"recommended"; |
- const wchar_t kSchema[] = L"schema"; |
-} // namespace registry_constants |
- |
namespace { |
-// Map of registry hives to their corresponding policy scope, in decreasing |
-// order of priority. |
-const struct { |
- HKEY hive; |
- PolicyScope scope; |
-} kHives[] = { |
- { HKEY_LOCAL_MACHINE, POLICY_SCOPE_MACHINE }, |
- { HKEY_CURRENT_USER, POLICY_SCOPE_USER }, |
+const char kKeyMandatory[] = "policy"; |
+const char kKeyRecommended[] = "recommended"; |
+const char kKeySchema[] = "schema"; |
+const char kKeyThirdParty[] = "3rdparty"; |
+ |
+// The GUID of the registry settings group policy extension. |
+GUID kRegistrySettingsCSEGUID = REGISTRY_EXTENSION_GUID; |
+ |
+// The PReg file name. |
+const base::FilePath::CharType kPRegFileName[] = |
+ FILE_PATH_LITERAL("Registry.pol"); |
+ |
+// A helper class encapsulating run-time-linked function calls to Wow64 APIs. |
+class Wow64Functions { |
+ public: |
+ Wow64Functions() |
+ : kernel32_lib_(base::FilePath(L"kernel32")), |
+ is_wow_64_process_(NULL), |
+ wow_64_disable_wow_64_fs_redirection_(NULL), |
+ wow_64_revert_wow_64_fs_redirection_(NULL) { |
+ if (kernel32_lib_.is_valid()) { |
+ is_wow_64_process_ = static_cast<IsWow64Process>( |
+ kernel32_lib_.GetFunctionPointer("IsWow64Process")); |
+ wow_64_disable_wow_64_fs_redirection_ = |
+ static_cast<Wow64DisableWow64FSRedirection>( |
+ kernel32_lib_.GetFunctionPointer( |
+ "Wow64DisableWow64FsRedirection")); |
+ wow_64_revert_wow_64_fs_redirection_ = |
+ static_cast<Wow64RevertWow64FSRedirection>( |
+ kernel32_lib_.GetFunctionPointer( |
+ "Wow64RevertWow64FsRedirection")); |
+ } |
+ } |
+ |
+ bool is_valid() { |
+ return is_wow_64_process_ && |
+ wow_64_disable_wow_64_fs_redirection_ && |
+ wow_64_revert_wow_64_fs_redirection_; |
+ } |
+ |
+ bool IsWow64() { |
+ BOOL result = 0; |
+ if (!is_wow_64_process_(GetCurrentProcess(), &result)) |
+ PLOG(WARNING) << "IsWow64ProcFailed"; |
+ return !!result; |
+ } |
+ |
+ bool DisableFsRedirection(PVOID* previous_state) { |
+ return !!wow_64_disable_wow_64_fs_redirection_(previous_state); |
+ } |
+ |
+ bool RevertFsRedirection(PVOID previous_state) { |
+ return !!wow_64_revert_wow_64_fs_redirection_(previous_state); |
+ } |
+ |
+ private: |
+ base::ScopedNativeLibrary kernel32_lib_; |
Joao da Silva
2013/04/10 12:32:12
nit: fields after types
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
Done.
|
+ |
+ typedef BOOL (WINAPI* IsWow64Process)(HANDLE, PBOOL); |
+ typedef BOOL (WINAPI* Wow64DisableWow64FSRedirection)(PVOID*); |
+ typedef BOOL (WINAPI* Wow64RevertWow64FSRedirection)(PVOID); |
+ |
+ IsWow64Process is_wow_64_process_; |
+ Wow64DisableWow64FSRedirection wow_64_disable_wow_64_fs_redirection_; |
+ Wow64RevertWow64FSRedirection wow_64_revert_wow_64_fs_redirection_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(Wow64Functions); |
}; |
-// Reads a REG_SZ string at |key| named |name| into |result|. Returns false if |
-// the string could not be read. |
-bool ReadRegistryString(RegKey* key, |
- const string16& name, |
- string16* result) { |
- DWORD value_size = 0; |
- DWORD key_type = 0; |
- scoped_array<uint8> buffer; |
+// Global Wow64Function instance used by ScopedDisableWow64Redirection below. |
+static base::LazyInstance<Wow64Functions> g_wow_64_functions = |
+ LAZY_INSTANCE_INITIALIZER; |
+ |
+// Scoper that switches off Wow64 File System Redirection during its lifetime. |
+class ScopedDisableWow64Redirection { |
+ public: |
+ ScopedDisableWow64Redirection() |
+ : active_(false), |
+ previous_state_(NULL) { |
+ Wow64Functions* wow64 = g_wow_64_functions.Pointer(); |
+ if (wow64->is_valid() && wow64->IsWow64()) { |
+ if (wow64->DisableFsRedirection(&previous_state_)) |
+ active_ = true; |
+ else |
+ PLOG(WARNING) << "Wow64DisableWow64FSRedirection"; |
+ } |
+ } |
- if (key->ReadValue(name.c_str(), 0, &value_size, &key_type) != ERROR_SUCCESS) |
- return false; |
- if (key_type != REG_SZ) |
- return false; |
+ ~ScopedDisableWow64Redirection() { |
+ if (active_) |
+ CHECK(g_wow_64_functions.Get().RevertFsRedirection(previous_state_)); |
+ } |
- // According to the Microsoft documentation, the string |
- // buffer may not be explicitly 0-terminated. Allocate a |
- // slightly larger buffer and pre-fill to zeros to guarantee |
- // the 0-termination. |
- buffer.reset(new uint8[value_size + 2]); |
- memset(buffer.get(), 0, value_size + 2); |
- key->ReadValue(name.c_str(), buffer.get(), &value_size, NULL); |
- result->assign(reinterpret_cast<const wchar_t*>(buffer.get())); |
- return true; |
+ bool is_active() { return active_; } |
+ |
+ private: |
+ bool active_; |
+ PVOID previous_state_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(ScopedDisableWow64Redirection); |
+}; |
+ |
+// AppliedGPOListProvider implementation that calls actual Windows APIs. |
+class WinGPOListProvider : public AppliedGPOListProvider { |
+ public: |
+ virtual ~WinGPOListProvider() {} |
+ |
+ // AppliedGPOListProvider: |
+ virtual DWORD GetAppliedGPOList(DWORD flags, |
+ LPCTSTR machine_name, |
+ PSID sid_user, |
+ GUID* extension_guid, |
+ PGROUP_POLICY_OBJECT* gpo_list) OVERRIDE{ |
Joao da Silva
2013/04/10 12:32:12
nit: space between OVERRIDE and {
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
Done.
|
+ return ::GetAppliedGPOList(flags, machine_name, sid_user, extension_guid, |
+ gpo_list); |
+ } |
+ |
+ virtual BOOL FreeGPOList(PGROUP_POLICY_OBJECT gpo_list) OVERRIDE { |
+ return ::FreeGPOList(gpo_list); |
+ } |
+}; |
+ |
+// The default windows GPO list provider used for PolicyLoaderWin. |
+static base::LazyInstance<WinGPOListProvider> g_win_gpo_list_provider = |
+ LAZY_INSTANCE_INITIALIZER; |
+ |
+// Returns the entry with key |name| in |dictionary| (can be NULL), or NULL. |
+const base::DictionaryValue* GetEntry(const base::DictionaryValue* dictionary, |
+ const std::string& name) { |
+ if (!dictionary) |
+ return NULL; |
+ const base::DictionaryValue* entry = NULL; |
+ dictionary->GetDictionaryWithoutPathExpansion(name, &entry); |
+ return entry; |
} |
-// Reads a REG_DWORD integer at |key| named |name| into |result|. Returns false |
-// if the value could no be read. |
-bool ReadRegistryInteger(RegKey* key, |
- const string16& name, |
- uint32* result) { |
- DWORD dword; |
- if (key->ReadValueDW(name.c_str(), &dword) != ERROR_SUCCESS) |
- return false; |
- *result = dword; |
- return true; |
+// Tries to extract the dictionary at |key| in |dict| and returns it. |
+scoped_ptr<base::DictionaryValue> RemoveDict(base::DictionaryValue* dict, |
+ const std::string& key) { |
+ base::Value* entry = NULL; |
+ base::DictionaryValue* result_dict = NULL; |
+ if (dict && dict->RemoveWithoutPathExpansion(key, &entry) && entry) { |
+ if (!entry->GetAsDictionary(&result_dict)) |
+ delete entry; |
+ } |
+ |
+ return make_scoped_ptr(result_dict); |
+} |
+ |
+std::string GetSchemaTypeForValueType(base::Value::Type value_type) { |
+ switch (value_type) { |
+ case base::Value::TYPE_DICTIONARY: |
+ return json_schema_constants::kObject; |
+ case base::Value::TYPE_INTEGER: |
+ return json_schema_constants::kInteger; |
+ case base::Value::TYPE_LIST: |
+ return json_schema_constants::kArray; |
+ case base::Value::TYPE_BOOLEAN: |
+ return json_schema_constants::kBoolean; |
+ case base::Value::TYPE_STRING: |
+ return json_schema_constants::kString; |
+ default: |
+ break; |
+ } |
+ |
+ NOTREACHED() << "Unsupported policy value type " << value_type; |
+ return json_schema_constants::kNull; |
} |
// Returns the Value type described in |schema|, or |default_type| if not found. |
-base::Value::Type GetType(const base::DictionaryValue* schema, |
- base::Value::Type default_type) { |
+base::Value::Type GetValueTypeForSchema(const base::DictionaryValue* schema, |
+ base::Value::Type default_type) { |
// JSON-schema types to base::Value::Type mapping. |
static const struct { |
// JSON schema type. |
@@ -115,7 +237,7 @@ base::Value::Type GetType(const base::DictionaryValue* schema, |
if (!schema) |
return default_type; |
std::string type; |
- if (!schema->GetString(schema::kType, &type)) |
+ if (!schema->GetStringWithoutPathExpansion(schema::kType, &type)) |
return default_type; |
for (size_t i = 0; i < arraysize(kSchemaToValueTypeMap); ++i) { |
if (type == kSchemaToValueTypeMap[i].schema_type) |
@@ -124,28 +246,11 @@ base::Value::Type GetType(const base::DictionaryValue* schema, |
return default_type; |
} |
-// Returns the default type for registry entries of |reg_type|, when there is |
-// no schema defined type for a policy. |
-base::Value::Type GetDefaultFor(DWORD reg_type) { |
- return reg_type == REG_DWORD ? base::Value::TYPE_INTEGER : |
- base::Value::TYPE_STRING; |
-} |
- |
-// Returns the entry with key |name| in |dictionary| (can be NULL), or NULL. |
-const base::DictionaryValue* GetEntry(const base::DictionaryValue* dictionary, |
- const std::string& name) { |
- if (!dictionary) |
- return NULL; |
- const base::DictionaryValue* entry = NULL; |
- dictionary->GetDictionary(name, &entry); |
- return entry; |
-} |
- |
// Returns the schema for property |name| given the |schema| of an object. |
// Returns the "additionalProperties" schema if no specific schema for |
// |name| is present. Returns NULL if no schema is found. |
const base::DictionaryValue* GetSchemaFor(const base::DictionaryValue* schema, |
- const std::string& name) { |
+ const std::string& name) { |
const base::DictionaryValue* properties = |
GetEntry(schema, schema::kProperties); |
const base::DictionaryValue* sub_schema = GetEntry(properties, name); |
@@ -155,241 +260,201 @@ const base::DictionaryValue* GetSchemaFor(const base::DictionaryValue* schema, |
return GetEntry(schema, schema::kAdditionalProperties); |
} |
-// Converts string |value| to another |type|, if possible. |
-base::Value* ConvertStringValue(const string16& value, base::Value::Type type) { |
- switch (type) { |
- case base::Value::TYPE_NULL: |
- return base::Value::CreateNullValue(); |
- |
- case base::Value::TYPE_BOOLEAN: { |
- int int_value; |
- if (base::StringToInt(value, &int_value)) |
- return base::Value::CreateBooleanValue(int_value != 0); |
- return NULL; |
+// Reads the subtree of the Windows registry at |root| into the passed |dict|. |
+void ReadRegistry(HKEY hive, |
+ const string16& root, |
+ base::DictionaryValue* dict) { |
+ // First, read all the values of the key. |
+ for (RegistryValueIterator it(hive, root.c_str()); it.Valid(); ++it) { |
+ const std::string name = UTF16ToUTF8(it.Name()); |
+ switch (it.Type()) { |
+ case REG_SZ: |
+ case REG_EXPAND_SZ: |
+ dict->SetStringWithoutPathExpansion(name, UTF16ToUTF8(it.Value())); |
+ continue; |
+ case REG_DWORD_LITTLE_ENDIAN: |
+ case REG_DWORD_BIG_ENDIAN: |
+ if (it.ValueSize() == sizeof(DWORD)) { |
+ DWORD dword_value = *(reinterpret_cast<const DWORD*>(it.Value())); |
+ if (it.Type() == REG_DWORD_BIG_ENDIAN) |
+ dword_value = base::NetToHost32(dword_value); |
+ else |
+ dword_value = base::ByteSwapToLE32(dword_value); |
+ dict->SetIntegerWithoutPathExpansion(name, dword_value); |
+ continue; |
+ } |
+ case REG_NONE: |
+ case REG_LINK: |
+ case REG_MULTI_SZ: |
+ case REG_RESOURCE_LIST: |
+ case REG_FULL_RESOURCE_DESCRIPTOR: |
+ case REG_RESOURCE_REQUIREMENTS_LIST: |
+ case REG_QWORD_LITTLE_ENDIAN: |
+ // Unsupported type, message gets logged below. |
+ break; |
} |
- case base::Value::TYPE_INTEGER: { |
- int int_value; |
- if (base::StringToInt(value, &int_value)) |
- return base::Value::CreateIntegerValue(int_value); |
- return NULL; |
- } |
+ LOG(WARNING) << "Failed to read hive " << hive << " at " |
+ << root << "\\" << name |
+ << " type " << it.Type(); |
+ } |
- case base::Value::TYPE_DOUBLE: { |
- double double_value; |
- if (base::StringToDouble(UTF16ToUTF8(value), &double_value)) |
- return base::Value::CreateDoubleValue(double_value); |
- DLOG(WARNING) << "Failed to read policy value as double: " << value; |
- return NULL; |
+ // Recurse for all subkeys. |
+ for (RegistryKeyIterator it(hive, root.c_str()); it.Valid(); ++it) { |
+ std::string name(UTF16ToUTF8(it.Name())); |
+ if (dict->HasKey(name)) { |
+ DLOG(WARNING) << "Ignoring registry key because a value exists with the " |
+ "same name: " << root << "\\" << name; |
+ } else { |
+ scoped_ptr<base::DictionaryValue> subdict(new base::DictionaryValue()); |
+ ReadRegistry(hive, root + L"\\" + it.Name(), subdict.get()); |
+ dict->SetWithoutPathExpansion(name, subdict.release()); |
} |
- |
- case base::Value::TYPE_STRING: |
- return base::Value::CreateStringValue(value); |
- |
- case base::Value::TYPE_DICTIONARY: |
- case base::Value::TYPE_LIST: |
- return base::JSONReader::Read(UTF16ToUTF8(value)); |
- |
- case base::Value::TYPE_BINARY: |
- DLOG(WARNING) << "Cannot convert REG_SZ entry to type " << type; |
- return NULL; |
} |
- NOTREACHED(); |
- return NULL; |
} |
-// Converts an integer |value| to another |type|, if possible. |
-base::Value* ConvertIntegerValue(uint32 value, base::Value::Type type) { |
- switch (type) { |
- case base::Value::TYPE_BOOLEAN: |
- return base::Value::CreateBooleanValue(value != 0); |
- |
- case base::Value::TYPE_INTEGER: |
- return base::Value::CreateIntegerValue(value); |
- |
- case base::Value::TYPE_DOUBLE: |
- return base::Value::CreateDoubleValue(value); |
- |
- case base::Value::TYPE_NULL: |
- case base::Value::TYPE_STRING: |
- case base::Value::TYPE_BINARY: |
- case base::Value::TYPE_DICTIONARY: |
- case base::Value::TYPE_LIST: |
- DLOG(WARNING) << "Cannot convert REG_DWORD entry to type " << type; |
- return NULL; |
+// Converts |value| in raw GPO representation to the internal policy value, as |
+// described by |schema|. This maps the ambiguous GPO data types to the |
+// internal policy value representations. |
+scoped_ptr<base::Value> ConvertPolicyValue( |
+ const base::Value& value, |
+ const base::DictionaryValue* schema) { |
+ // Figure out the type to convert to from the schema. |
+ const base::Value::Type result_type( |
+ GetValueTypeForSchema(schema, value.GetType())); |
+ |
+ // If the type is good already, go with it. |
+ if (value.IsType(result_type)) { |
+ // Recurse for complex types if there is a schema. |
+ if (schema) { |
+ const base::DictionaryValue* dict = NULL; |
+ const base::ListValue* list = NULL; |
+ if (value.GetAsDictionary(&dict)) { |
+ scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue()); |
+ for (base::DictionaryValue::Iterator entry(*dict); entry.HasNext(); |
+ entry.Advance()) { |
+ scoped_ptr<base::Value> converted_value( |
+ ConvertPolicyValue(entry.value(), |
+ GetSchemaFor(schema, entry.key()))); |
+ result->SetWithoutPathExpansion(entry.key(), |
+ converted_value.release()); |
+ } |
+ return result.Pass(); |
+ } else if (value.GetAsList(&list)) { |
+ scoped_ptr<base::ListValue> result(new base::ListValue()); |
+ const base::DictionaryValue* item_schema = |
+ GetEntry(schema, schema::kItems); |
+ for (base::ListValue::const_iterator entry(list->begin()); |
+ entry != list->end(); ++entry) { |
+ result->Append(ConvertPolicyValue(**entry, item_schema).release()); |
+ } |
+ return result.Pass(); |
+ } |
+ } |
+ return make_scoped_ptr(value.DeepCopy()); |
} |
- NOTREACHED(); |
- return NULL; |
-} |
-// Reads a value from the registry |key| named |name| with registry type |
-// |registry_type| as a value of type |type|. |
-// Returns NULL if the value could not be loaded or converted. |
-base::Value* ReadPolicyValue(RegKey* key, |
- const string16& name, |
- DWORD registry_type, |
- base::Value::Type type) { |
- switch (registry_type) { |
- case REG_SZ: { |
- string16 value; |
- if (ReadRegistryString(key, name, &value)) |
- return ConvertStringValue(value, type); |
- break; |
+ // Else, do some conversions to map windows registry data types to JSON types. |
+ std::string string_value; |
+ int int_value = 0; |
+ switch (result_type) { |
+ case base::Value::TYPE_NULL: { |
+ return make_scoped_ptr(base::Value::CreateNullValue()); |
} |
- |
- case REG_DWORD: { |
- uint32 value; |
- if (ReadRegistryInteger(key, name, &value)) |
- return ConvertIntegerValue(value, type); |
+ case base::Value::TYPE_BOOLEAN: { |
+ // Accept booleans encoded as either string or integer. |
+ if (value.GetAsInteger(&int_value) || |
+ (value.GetAsString(&string_value) && |
+ base::StringToInt(string_value, &int_value))) { |
+ return make_scoped_ptr(Value::CreateBooleanValue(int_value != 0)); |
+ } |
break; |
} |
- |
- default: |
- DLOG(WARNING) << "Registry type not supported for key " << name; |
+ case base::Value::TYPE_INTEGER: { |
+ // Integers may be string-encoded. |
+ if (value.GetAsString(&string_value) && |
+ base::StringToInt(string_value, &int_value)) { |
+ return make_scoped_ptr(base::Value::CreateIntegerValue(int_value)); |
+ } |
break; |
- } |
- return NULL; |
-} |
- |
-// Forward declaration for ReadComponentListValue(). |
-base::DictionaryValue* ReadComponentDictionaryValue( |
- HKEY hive, |
- const string16& path, |
- const base::DictionaryValue* schema); |
- |
-// Loads the list at |path| in the given |hive|. |schema| is a JSON schema |
-// (http://json-schema.org/) that describes the expected type of the list. |
-// Ownership of the result is transferred to the caller. |
-base::ListValue* ReadComponentListValue(HKEY hive, |
- const string16& path, |
- const base::DictionaryValue* schema) { |
- // The sub-elements are indexed from 1 to N. They can be represented as |
- // registry values or registry keys though; use |schema| first to try to |
- // determine the right type, and if that fails default to STRING. |
- |
- RegKey key(hive, path.c_str(), KEY_READ); |
- if (!key.Valid()) |
- return NULL; |
- |
- // Get the schema for list items. |
- schema = GetEntry(schema, schema::kItems); |
- base::Value::Type type = GetType(schema, base::Value::TYPE_STRING); |
- base::ListValue* list = new base::ListValue(); |
- for (int i = 1; ; ++i) { |
- string16 name = base::IntToString16(i); |
- base::Value* value = NULL; |
- if (type == base::Value::TYPE_DICTIONARY) { |
- value = |
- ReadComponentDictionaryValue(hive, path + kPathSep + name, schema); |
- } else if (type == base::Value::TYPE_LIST) { |
- value = ReadComponentListValue(hive, path + kPathSep + name, schema); |
- } else { |
- DWORD reg_type; |
- key.ReadValue(name.c_str(), NULL, NULL, ®_type); |
- if (reg_type != REG_NONE) |
- value = ReadPolicyValue(&key, name, reg_type, type); |
} |
- if (value) |
- list->Append(value); |
- else |
+ case base::Value::TYPE_DOUBLE: { |
+ // Doubles may be string-encoded or integer-encoded. |
+ double double_value = 0; |
+ if (value.GetAsInteger(&int_value)) { |
+ return make_scoped_ptr(base::Value::CreateDoubleValue(int_value)); |
+ } else if (value.GetAsString(&string_value) && |
+ base::StringToDouble(string_value, &double_value)) { |
+ return make_scoped_ptr(base::Value::CreateDoubleValue(double_value)); |
+ } |
break; |
- } |
- return list; |
-} |
- |
-// Loads the dictionary at |path| in the given |hive|. |schema| is a JSON |
-// schema (http://json-schema.org/) that describes the expected types for the |
-// dictionary entries. When the type for a certain entry isn't described in the |
-// schema, a default conversion takes place. |schema| can be NULL. |
-// Ownership of the result is transferred to the caller. |
-base::DictionaryValue* ReadComponentDictionaryValue( |
- HKEY hive, |
- const string16& path, |
- const base::DictionaryValue* schema) { |
- // A "value" in the registry is like a file in a filesystem, and a "key" is |
- // like a directory, that contains other "values" and "keys". |
- // Unfortunately it is possible to have a name both as a "value" and a "key". |
- // In those cases, the sub "key" will be ignored; this choice is arbitrary. |
- |
- // First iterate over all the "values" in |path| and convert them; then |
- // recurse into each "key" in |path| and convert them as dictionaries. |
- |
- RegKey key(hive, path.c_str(), KEY_READ); |
- if (!key.Valid()) |
- return NULL; |
- |
- base::DictionaryValue* dict = new base::DictionaryValue(); |
- for (RegistryValueIterator it(hive, path.c_str()); it.Valid(); ++it) { |
- string16 name16(it.Name()); |
- std::string name(UTF16ToUTF8(name16)); |
- const base::DictionaryValue* sub_schema = GetSchemaFor(schema, name); |
- base::Value::Type type = GetType(sub_schema, GetDefaultFor(it.Type())); |
- base::Value* value = ReadPolicyValue(&key, name16, it.Type(), type); |
- if (value) |
- dict->Set(name, value); |
- } |
- |
- for (RegistryKeyIterator it(hive, path.c_str()); it.Valid(); ++it) { |
- string16 name16(it.Name()); |
- std::string name(UTF16ToUTF8(name16)); |
- if (dict->HasKey(name)) { |
- DLOG(WARNING) << "Ignoring registry key because a value exists with the " |
- "same name: " << path << kPathSep << name; |
- continue; |
} |
- |
- const base::DictionaryValue* sub_schema = GetSchemaFor(schema, name); |
- base::Value::Type type = GetType(sub_schema, base::Value::TYPE_DICTIONARY); |
- base::Value* value = NULL; |
- const string16 sub_path = path + kPathSep + name16; |
- if (type == base::Value::TYPE_DICTIONARY) { |
- value = ReadComponentDictionaryValue(hive, sub_path, sub_schema); |
- } else if (type == base::Value::TYPE_LIST) { |
- value = ReadComponentListValue(hive, sub_path, sub_schema); |
- } else { |
- DLOG(WARNING) << "Can't read a simple type in registry key at " << path; |
+ case base::Value::TYPE_LIST: { |
+ // Lists are encoded as subkeys with numbered value in the registry. |
+ const base::DictionaryValue* dict = NULL; |
+ if (value.GetAsDictionary(&dict)) { |
+ scoped_ptr<base::ListValue> result(new base::ListValue()); |
+ const base::DictionaryValue* item_schema = |
+ GetEntry(schema, schema::kItems); |
+ for (int i = 1; ; ++i) { |
+ const base::Value* entry = NULL; |
+ if (!dict->Get(base::IntToString(i), &entry)) |
+ break; |
+ result->Append(ConvertPolicyValue(*entry, item_schema).release()); |
+ } |
+ return result.Pass(); |
+ } |
+ // Fall through in order to accept lists encoded as JSON strings. |
} |
- if (value) |
- dict->Set(name, value); |
+ case base::Value::TYPE_DICTIONARY: { |
+ // Dictionaries may be encoded as JSON strings. |
+ if (value.GetAsString(&string_value)) { |
+ scoped_ptr<base::Value> result(base::JSONReader::Read(string_value)); |
+ if (result && result->IsType(result_type)) |
+ return result.Pass(); |
+ } |
+ break; |
+ } |
+ case base::Value::TYPE_STRING: |
+ case base::Value::TYPE_BINARY: |
+ // No conversion possible. |
+ break; |
} |
- return dict; |
+ LOG(WARNING) << "Failed to convert " << value.GetType() |
+ << " to " << result_type; |
+ return make_scoped_ptr(base::Value::CreateNullValue()); |
} |
-// Reads a JSON schema from the given |registry_value|, at the given |
-// |registry_key| in |hive|. |registry_value| must be a string (REG_SZ), and |
-// is decoded as JSON data. Returns NULL on failure. Ownership is transferred |
-// to the caller. |
-base::DictionaryValue* ReadRegistrySchema(HKEY hive, |
- const string16& registry_key, |
- const string16& registry_value) { |
- RegKey key(hive, registry_key.c_str(), KEY_READ); |
- string16 schema; |
- if (!ReadRegistryString(&key, registry_value, &schema)) |
- return NULL; |
- // A JSON schema is represented in JSON too. |
- scoped_ptr<base::Value> value(base::JSONReader::Read(UTF16ToUTF8(schema))); |
- if (!value.get()) |
- return NULL; |
- base::DictionaryValue* dict = NULL; |
- if (!value->GetAsDictionary(&dict)) |
- return NULL; |
- // The top-level entry must be an object, and each of its properties maps |
- // a policy name to its schema. |
- if (GetType(dict, base::Value::TYPE_DICTIONARY) != |
- base::Value::TYPE_DICTIONARY) { |
- DLOG(WARNING) << "schema top-level type isn't \"object\""; |
- return NULL; |
+// Parses |gpo_dict| according to |schema| and writes the resulting policy |
+// settings to |policy| for the given |scope| and |level|. |
+void ParsePolicy(const base::DictionaryValue* gpo_dict, |
+ PolicyLevel level, |
+ PolicyScope scope, |
+ const base::DictionaryValue* schema, |
+ PolicyMap* policy) { |
+ if (!gpo_dict) |
+ return; |
+ |
+ scoped_ptr<base::Value> policy_value(ConvertPolicyValue(*gpo_dict, schema)); |
+ const base::DictionaryValue* policy_dict = NULL; |
+ if (!policy_value->GetAsDictionary(&policy_dict) || !policy_dict) { |
+ LOG(WARNING) << "Root policy object is not a dictionary!"; |
+ return; |
} |
- value.release(); |
- return dict; |
+ |
+ policy->LoadFrom(policy_dict, level, scope); |
} |
} // namespace |
-PolicyLoaderWin::PolicyLoaderWin(const PolicyDefinitionList* policy_list) |
+PolicyLoaderWin::PolicyLoaderWin(const PolicyDefinitionList* policy_list, |
+ const string16& chrome_policy_key, |
+ AppliedGPOListProvider* gpo_provider) |
: is_initialized_(false), |
+ chrome_policy_key_(chrome_policy_key), |
policy_list_(policy_list), |
Joao da Silva
2013/04/10 12:32:12
The order in the header is different.
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
Done.
|
+ gpo_provider_(gpo_provider), |
user_policy_changed_event_(false, false), |
machine_policy_changed_event_(false, false), |
user_policy_watcher_failed_(false), |
@@ -409,155 +474,259 @@ PolicyLoaderWin::~PolicyLoaderWin() { |
machine_policy_watcher_.StopWatching(); |
} |
+// static |
+scoped_ptr<PolicyLoaderWin> PolicyLoaderWin::Create( |
+ const PolicyDefinitionList* policy_list) { |
+ return make_scoped_ptr( |
+ new PolicyLoaderWin(policy_list, kRegistryChromePolicyKey, |
+ g_win_gpo_list_provider.Pointer())); |
+} |
+ |
void PolicyLoaderWin::InitOnFile() { |
is_initialized_ = true; |
SetupWatches(); |
} |
scoped_ptr<PolicyBundle> PolicyLoaderWin::Load() { |
- scoped_ptr<PolicyBundle> bundle(new PolicyBundle()); |
- LoadChromePolicy( |
- &bundle->Get(PolicyNamespace(POLICY_DOMAIN_CHROME, std::string()))); |
- Load3rdPartyPolicies(bundle.get()); |
- return bundle.Pass(); |
-} |
- |
-void PolicyLoaderWin::LoadChromePolicy(PolicyMap* chrome_policies) { |
// Reset the watches BEFORE reading the individual policies to avoid |
// missing a change notification. |
if (is_initialized_) |
SetupWatches(); |
- // |kKeyPaths| is in decreasing order of priority. |
+ if (chrome_policy_schema_.empty()) |
+ BuildChromePolicySchema(); |
+ |
+ // Policy scope and corresponding hive. |
static const struct { |
- const wchar_t* path; |
- PolicyLevel level; |
- } kKeyPaths[] = { |
- { kRegistryMandatorySubKey, POLICY_LEVEL_MANDATORY }, |
- { kRegistryRecommendedSubKey, POLICY_LEVEL_RECOMMENDED }, |
+ PolicyScope scope; |
+ HKEY hive; |
+ } kScopes[] = { |
+ { POLICY_SCOPE_MACHINE, HKEY_LOCAL_MACHINE }, |
+ { POLICY_SCOPE_USER, HKEY_CURRENT_USER }, |
}; |
- // Lookup at the mandatory path for both user and machine policies first, and |
- // then at the recommended path. |
- for (size_t k = 0; k < arraysize(kKeyPaths); ++k) { |
- for (size_t h = 0; h < arraysize(kHives); ++h) { |
- // Iterate over keys and values at this hive and path. |
- HKEY hive = kHives[h].hive; |
- string16 path(kKeyPaths[k].path); |
- RegKey key; |
- if (key.Open(hive, path.c_str(), KEY_READ) != ERROR_SUCCESS || |
- !key.Valid()) { |
- continue; |
- } |
+ // Load policy data for the different scopes/levels and merge them. |
+ scoped_ptr<PolicyBundle> bundle(new PolicyBundle()); |
+ PolicyMap* chrome_policy = |
+ &bundle->Get(PolicyNamespace(POLICY_DOMAIN_CHROME, std::string())); |
+ for (size_t i = 0; i < arraysize(kScopes); ++i) { |
+ PolicyScope scope = kScopes[i].scope; |
+ base::DictionaryValue gpo_dict; |
+ |
+ HANDLE policy_lock = |
+ EnterCriticalPolicySection(scope == POLICY_SCOPE_MACHINE); |
Joao da Silva
2013/04/10 12:32:12
Is this needed to read from the registry? If not t
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
This makes sure GPO doesn't update behind our back
|
+ if (policy_lock == NULL) |
+ PLOG(ERROR) << "EnterCriticalPolicySection"; |
+ |
+ if (!ReadPolicyFromGPO(scope, &gpo_dict)) { |
+ VLOG(1) << "Failed to read GPO files for " << scope |
+ << " falling back to registry."; |
+ ReadRegistry(kScopes[i].hive, chrome_policy_key_, &gpo_dict); |
+ } |
- // Iterate over values for most policies. |
- for (RegistryValueIterator it(hive, path.c_str()); it.Valid(); ++it) { |
- std::string name(UTF16ToUTF8(it.Name())); |
- // Skip if a higher-priority policy value was already inserted, or |
- // if this is the default value (empty string). |
- if (chrome_policies->Get(name) || name.empty()) |
- continue; |
- // Get the expected policy type, if this is a known policy. |
- base::Value::Type type = GetDefaultFor(it.Type()); |
- for (const PolicyDefinitionList::Entry* e = policy_list_->begin; |
- e != policy_list_->end; ++e) { |
- if (name == e->name) { |
- type = e->value_type; |
- break; |
- } |
- } |
- base::Value* value = ReadPolicyValue(&key, it.Name(), it.Type(), type); |
- if (!value) |
- value = base::Value::CreateNullValue(); |
- chrome_policies->Set(name, kKeyPaths[k].level, kHives[h].scope, value); |
- } |
+ if (!LeaveCriticalPolicySection(policy_lock)) |
Joao da Silva
2013/04/10 12:32:12
Is this safe if Enter failed?
Mattias Nissler (ping if slow)
2013/04/10 13:20:25
Yes and no :) Yes, because there's no reason why C
|
+ PLOG(ERROR) << "LeaveCriticalPolicySection"; |
- // Iterate over keys for policies of type string-list. |
- for (RegistryKeyIterator it(hive, path.c_str()); it.Valid(); ++it) { |
- std::string name(UTF16ToUTF8(it.Name())); |
- // Skip if a higher-priority policy value was already inserted, or |
- // if this is the 3rd party policy subkey. |
- const string16 kThirdParty16(kThirdParty); |
- if (chrome_policies->Get(name) || it.Name() == kThirdParty16) |
- continue; |
- string16 list_path = path + kPathSep + it.Name(); |
- RegKey key; |
- if (key.Open(hive, list_path.c_str(), KEY_READ) != ERROR_SUCCESS || |
- !key.Valid()) { |
- continue; |
- } |
- base::ListValue* result = new base::ListValue(); |
- string16 value; |
- int index = 0; |
- while (ReadRegistryString(&key, base::IntToString16(++index), &value)) |
- result->Append(base::Value::CreateStringValue(value)); |
- chrome_policies->Set(name, kKeyPaths[k].level, kHives[h].scope, result); |
- } |
+ // Remove special-cased entries from the GPO dictionary. |
+ base::DictionaryValue* temp_dict = NULL; |
+ scoped_ptr<base::DictionaryValue> recommended_dict( |
+ RemoveDict(&gpo_dict, kKeyRecommended)); |
+ scoped_ptr<base::DictionaryValue> third_party_dict( |
+ RemoveDict(&gpo_dict, 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.Pass(); |
+} |
+ |
+void PolicyLoaderWin::BuildChromePolicySchema() { |
+ scoped_ptr<base::DictionaryValue> properties(new base::DictionaryValue()); |
+ for (const PolicyDefinitionList::Entry* e = policy_list_->begin; |
+ e != policy_list_->end; ++e) { |
+ const std::string schema_type = GetSchemaTypeForValueType(e->value_type); |
+ scoped_ptr<base::DictionaryValue> entry_schema(new base::DictionaryValue()); |
+ entry_schema->SetStringWithoutPathExpansion(json_schema_constants::kType, |
+ schema_type); |
+ |
+ if (e->value_type == base::Value::TYPE_LIST) { |
+ scoped_ptr<base::DictionaryValue> items_schema( |
+ new base::DictionaryValue()); |
+ items_schema->SetStringWithoutPathExpansion( |
+ json_schema_constants::kType, json_schema_constants::kString); |
+ entry_schema->SetWithoutPathExpansion(json_schema_constants::kItems, |
+ items_schema.release()); |
} |
+ properties->SetWithoutPathExpansion(e->name, entry_schema.release()); |
} |
+ chrome_policy_schema_.SetStringWithoutPathExpansion( |
+ json_schema_constants::kType, json_schema_constants::kObject); |
+ chrome_policy_schema_.SetWithoutPathExpansion( |
+ json_schema_constants::kProperties, properties.release()); |
} |
-void PolicyLoaderWin::Load3rdPartyPolicies(PolicyBundle* bundle) { |
- // Each 3rd party namespace can have policies on both HKLM and HKCU. They |
- // should be merged, giving priority to HKLM for policies with the same name. |
+bool PolicyLoaderWin::ReadPRegFile(const base::FilePath& preg_file, |
+ base::DictionaryValue* policy) { |
+ // The following deals with the minor annoyance that Wow64 FS redirection |
+ // might need to be turned off: This is the case if running as a 32-bit |
+ // process on a 64-bit system, in which case Wow64 FS redirection redirects |
+ // access to the %WINDIR%/System32/GroupPolicy directory to |
+ // %WINDIR%/SysWOW64/GroupPolicy, but the file is actually in the |
+ // system-native directory. |
+ if (file_util::PathExists(preg_file)) { |
+ return preg_parser::ReadFile(preg_file, chrome_policy_key_, policy); |
+ } else { |
+ // Try with redirection switched off. |
+ ScopedDisableWow64Redirection redirection_disable; |
+ if (redirection_disable.is_active() && file_util::PathExists(preg_file)) |
+ return preg_parser::ReadFile(preg_file, chrome_policy_key_, policy); |
+ } |
+ |
+ // Report the error. |
+ LOG(ERROR) << "PReg file doesn't exist: " << preg_file.value(); |
+ return false; |
+} |
+ |
+bool PolicyLoaderWin::LoadGPOPolicy(PolicyScope scope, |
+ PGROUP_POLICY_OBJECT policy_object_list, |
+ base::DictionaryValue* policy) { |
+ base::DictionaryValue parsed_policy; |
+ base::DictionaryValue forced_policy; |
+ for (GROUP_POLICY_OBJECT* policy_object = policy_object_list; |
+ policy_object; policy_object = policy_object->pNext) { |
+ if (policy_object->dwOptions & GPO_FLAG_DISABLE) |
+ continue; |
+ |
+ if (PathIsUNC(policy_object->lpFileSysPath)) { |
+ // UNC path: Assume this is an AD-managed machine, which updates the |
+ // registry via GPO's standard registry CSE periodically. Fall back to |
+ // reading from the registry in this case. |
+ return false; |
+ } |
+ |
+ base::FilePath preg_file_path( |
+ base::FilePath(policy_object->lpFileSysPath).Append(kPRegFileName)); |
+ if (policy_object->dwOptions & GPO_FLAG_FORCE) { |
+ base::DictionaryValue new_forced_policy; |
+ if (!ReadPRegFile(preg_file_path, &new_forced_policy)) |
+ return false; |
+ |
+ // Merge with existing forced policy, giving precedence to the existing |
+ // forced policy. |
+ new_forced_policy.MergeDictionary(&forced_policy); |
+ forced_policy.Swap(&new_forced_policy); |
+ } else { |
+ if (!ReadPRegFile(preg_file_path, &parsed_policy)) |
+ return false; |
+ } |
+ } |
+ |
+ // Merge, give precedence to forced policy. |
+ parsed_policy.MergeDictionary(&forced_policy); |
+ policy->Swap(&parsed_policy); |
- // Map of known domain name to their enum values. |
+ return true; |
+} |
+ |
+ |
+bool PolicyLoaderWin::ReadPolicyFromGPO(PolicyScope scope, |
+ base::DictionaryValue* policy) { |
+ PGROUP_POLICY_OBJECT policy_object_list = NULL; |
+ DWORD flags = scope == POLICY_SCOPE_MACHINE ? GPO_LIST_FLAG_MACHINE : 0; |
+ if (gpo_provider_->GetAppliedGPOList( |
+ flags, NULL, NULL, &kRegistrySettingsCSEGUID, |
+ &policy_object_list) != ERROR_SUCCESS) { |
+ PLOG(ERROR) << "GetAppliedGPOList scope " << scope; |
+ return false; |
+ } |
+ |
+ bool result = LoadGPOPolicy(scope, policy_object_list, policy); |
+ if (!gpo_provider_->FreeGPOList(policy_object_list)) |
+ LOG(WARNING) << "FreeGPOList"; |
+ |
+ return result; |
+} |
+ |
+void PolicyLoaderWin::LoadChromePolicy(const base::DictionaryValue* gpo_dict, |
+ PolicyLevel level, |
+ PolicyScope scope, |
+ PolicyMap* chrome_policy_map) { |
+ PolicyMap policy; |
+ ParsePolicy(gpo_dict, level, scope, &chrome_policy_schema_, &policy); |
+ chrome_policy_map->MergeFrom(policy); |
+} |
+ |
+void PolicyLoaderWin::Load3rdPartyPolicy( |
+ const DictionaryValue* 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; |
- } kDomains[] = { |
- { "extensions", POLICY_DOMAIN_EXTENSIONS }, |
+ } k3rdPartyDomains[] = { |
+ { "extensions", POLICY_DOMAIN_EXTENSIONS }, |
}; |
- // Map of policy paths to their corresponding policy level, in decreasing |
- // order of priority. |
+ // Policy level and corresponding path. |
static const struct { |
- const char* path; |
PolicyLevel level; |
- } kKeyPaths[] = { |
- { "policy", POLICY_LEVEL_MANDATORY }, |
- { "recommended", POLICY_LEVEL_RECOMMENDED }, |
+ const char* path; |
+ } kLevels[] = { |
+ { POLICY_LEVEL_MANDATORY, kKeyMandatory }, |
+ { POLICY_LEVEL_RECOMMENDED, kKeyRecommended }, |
}; |
- // Path where policies for components are stored. |
- const string16 kPathPrefix = string16(kRegistryMandatorySubKey) + kPathSep + |
- kThirdParty + kPathSep; |
- |
- for (size_t h = 0; h < arraysize(kHives); ++h) { |
- HKEY hkey = kHives[h].hive; |
- |
- for (size_t d = 0; d < arraysize(kDomains); ++d) { |
- // Each subkey under this domain is a component of that domain. |
- // |domain_path| == SOFTWARE\Policies\Chromium\3rdparty\<domain> |
- string16 domain_path = kPathPrefix + ASCIIToUTF16(kDomains[d].name); |
- |
- for (RegistryKeyIterator domain_iterator(hkey, domain_path.c_str()); |
- domain_iterator.Valid(); ++domain_iterator) { |
- string16 component(domain_iterator.Name()); |
- string16 component_path = domain_path + kPathSep + component; |
- |
- // Load the schema for this component's policy, if present. |
- scoped_ptr<base::DictionaryValue> schema( |
- ReadRegistrySchema(hkey, component_path, kSchema)); |
- |
- for (size_t k = 0; k < arraysize(kKeyPaths); ++k) { |
- string16 path = |
- component_path + kPathSep + ASCIIToUTF16(kKeyPaths[k].path); |
- |
- scoped_ptr<base::DictionaryValue> dictionary( |
- ReadComponentDictionaryValue(hkey, path, schema.get())); |
- if (dictionary.get()) { |
- PolicyMap policies; |
- policies.LoadFrom( |
- dictionary.get(), kKeyPaths[k].level, kHives[h].scope); |
- // LoadFrom() overwrites any existing values. Use a temporary map |
- // and then use MergeFrom(), that only overwrites values with lower |
- // priority. |
- bundle->Get(PolicyNamespace(kDomains[d].domain, |
- UTF16ToUTF8(component))) |
- .MergeFrom(policies); |
- } |
+ for (size_t i = 0; i < arraysize(k3rdPartyDomains); i++) { |
+ const char* name = k3rdPartyDomains[i].name; |
+ const PolicyDomain domain = k3rdPartyDomains[i].domain; |
+ const base::DictionaryValue* domain_dict = NULL; |
+ if (!gpo_dict->GetDictionaryWithoutPathExpansion(name, &domain_dict) || |
+ !domain_dict) { |
+ continue; |
+ } |
+ |
+ for (base::DictionaryValue::Iterator component(*domain_dict); |
+ component.HasNext(); component.Advance()) { |
+ const base::DictionaryValue* component_dict = NULL; |
+ if (!component.value().GetAsDictionary(&component_dict) || |
+ !component_dict) { |
+ continue; |
+ } |
+ |
+ // Load the schema. |
+ scoped_ptr<base::Value> schema; |
+ const base::DictionaryValue* schema_dict = NULL; |
+ std::string schema_json; |
+ if (component_dict->GetStringWithoutPathExpansion(kKeySchema, |
+ &schema_json)) { |
+ schema.reset(base::JSONReader::Read(schema_json)); |
+ if (!schema || !schema->GetAsDictionary(&schema_dict)) { |
+ LOG(WARNING) << "Failed to parse 3rd-part policy schema for " |
+ << domain << "/" << component.key(); |
+ } |
+ } |
+ |
+ // Parse policy. |
+ for (size_t j = 0; j < arraysize(kLevels); j++) { |
+ const base::DictionaryValue* policy_dict = NULL; |
+ if (!component_dict->GetDictionaryWithoutPathExpansion( |
+ kLevels[j].path, &policy_dict) || |
+ !policy_dict) { |
+ continue; |
} |
+ |
+ PolicyMap policy; |
+ ParsePolicy(policy_dict, kLevels[j].level, scope, schema_dict, &policy); |
+ PolicyNamespace policy_namespace(domain, component.key()); |
+ bundle->Get(policy_namespace).MergeFrom(policy); |
} |
} |
} |