blob: de0595cb5093c2d81810e7e5da8a109eb829a6a9 [file] [log] [blame]
// Copyright 2020 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 "chrome/browser/device_api/managed_configuration_api.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/contains.h"
#include "base/json/json_string_value_serializer.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/device_api/managed_configuration_store.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/origin.h"
namespace {
const char kManagedConfigurationDirectoryName[] = "Managed Configuration";
// Maximum configuration size is 5MB.
constexpr int kMaxConfigurationFileSize = 5 * 1024 * 1024;
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("managed_configuration_loader",
R"(
semantics {
sender: "Web App Managed Configuration Downloader"
description:
"Fetches JSON managed configuration from the DM server, for the "
"origins configured in the ManagedConfigurationPerOrigin policy. "
"This enables the navigatior.device.getManagedConfiguration(keys) "
"JavaScript function, to obtain the values of this configuration "
"that correspond to provided keys. This configuration can only be "
"set for force-installed web applications, which are defined by "
"WebAppInstallForceList policy."
trigger:
"Changes in the ManagedConfigurationPerOrigin policy."
data: "No user data."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled in settings, but it will only be "
"sent if the device administrator sets up a configuration policy "
"for a particular origin.",
chrome_policy {
ManagedConfigurationPerOrigin {
ManagedConfigurationPerOrigin: "[]"
}
}
})");
// Converts url::Origin into the key that can be used for filenames/dictionary
// keys.
std::string GetOriginEncoded(const url::Origin& origin) {
std::string serialized = origin.Serialize();
return base::HexEncode(serialized.data(), serialized.size());
}
} // namespace
const char ManagedConfigurationAPI::kOriginKey[] = "origin";
const char ManagedConfigurationAPI::kManagedConfigurationUrlKey[] =
"managed_configuration_url";
const char ManagedConfigurationAPI::kManagedConfigurationHashKey[] =
"managed_configuration_hash";
class ManagedConfigurationAPI::ManagedConfigurationDownloader {
public:
explicit ManagedConfigurationDownloader(const std::string& data_hash)
: data_hash_(data_hash) {}
~ManagedConfigurationDownloader() = default;
void Fetch(const std::string& data_url,
base::OnceCallback<void(std::unique_ptr<std::string>)> callback) {
// URLLoaders should be created at UI thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(data_url);
simple_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), kTrafficAnnotation);
simple_loader_->SetRetryOptions(
/* max_retries=*/3,
network::SimpleURLLoader::RETRY_ON_5XX |
network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
SystemNetworkContextManager* system_network_context_manager =
g_browser_process->system_network_context_manager();
network::mojom::URLLoaderFactory* loader_factory =
system_network_context_manager->GetURLLoaderFactory();
simple_loader_->DownloadToString(loader_factory, std::move(callback),
kMaxConfigurationFileSize);
}
std::string hash() const { return data_hash_; }
private:
std::unique_ptr<network::SimpleURLLoader> simple_loader_;
const std::string data_hash_;
};
ManagedConfigurationAPI::ManagedConfigurationAPI(Profile* profile)
: profile_(profile),
stores_path_(
profile->GetPath().AppendASCII(kManagedConfigurationDirectoryName)),
backend_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(profile_->GetPrefs());
pref_change_registrar_->Add(
prefs::kManagedConfigurationPerOrigin,
base::BindRepeating(
&ManagedConfigurationAPI::OnConfigurationPolicyChanged,
weak_ptr_factory_.GetWeakPtr()));
OnConfigurationPolicyChanged();
}
ManagedConfigurationAPI::~ManagedConfigurationAPI() = default;
// static
void ManagedConfigurationAPI::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kManagedConfigurationPerOrigin);
registry->RegisterDictionaryPref(
prefs::kLastManagedConfigurationHashForOrigin);
}
void ManagedConfigurationAPI::GetOriginPolicyConfiguration(
const url::Origin& origin,
const std::vector<std::string>& keys,
base::OnceCallback<void(std::unique_ptr<base::DictionaryValue>)> callback) {
if (!CanHaveManagedStore(origin)) {
return std::move(callback).Run(nullptr);
}
backend_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ManagedConfigurationAPI::GetConfigurationOnBackend,
base::Unretained(this), origin, keys),
std::move(callback));
}
void ManagedConfigurationAPI::AddObserver(Observer* observer) {
if (CanHaveManagedStore(observer->GetOrigin())) {
GetOrLoadStoreForOrigin(observer->GetOrigin())->AddObserver(observer);
} else {
unmanaged_observers_.insert(observer);
}
}
void ManagedConfigurationAPI::RemoveObserver(Observer* observer) {
auto it = unmanaged_observers_.find(observer);
if (it != unmanaged_observers_.end()) {
unmanaged_observers_.erase(it);
return;
}
GetOrLoadStoreForOrigin(observer->GetOrigin())->RemoveObserver(observer);
}
bool ManagedConfigurationAPI::CanHaveManagedStore(const url::Origin& origin) {
return base::Contains(managed_origins_, origin);
}
const std::set<url::Origin>& ManagedConfigurationAPI::GetManagedOrigins()
const {
return managed_origins_;
}
void ManagedConfigurationAPI::OnConfigurationPolicyChanged() {
const base::Value* managed_configurations =
profile_->GetPrefs()->GetList(prefs::kManagedConfigurationPerOrigin);
std::set<url::Origin> current_origins;
for (const auto& entry : managed_configurations->GetList()) {
const std::string* url = entry.FindStringKey(kOriginKey);
if (!url)
continue;
const url::Origin origin = url::Origin::Create(GURL(*url));
if (origin.opaque())
continue;
const std::string* configuration_url =
entry.FindStringKey(kManagedConfigurationUrlKey);
const std::string* configuration_hash =
entry.FindStringKey(kManagedConfigurationHashKey);
current_origins.insert(origin);
if (!configuration_url || !configuration_hash)
continue;
UpdateStoredDataForOrigin(origin, *configuration_url, *configuration_hash);
}
// We need to clean configurations for origins that got their entry removed.
for (const auto& store_entry : store_map_) {
if (!base::Contains(current_origins, store_entry.first)) {
UpdateStoredDataForOrigin(store_entry.first, std::string(),
std::string());
}
}
managed_origins_.swap(current_origins);
PromoteObservers();
}
ManagedConfigurationStore* ManagedConfigurationAPI::GetOrLoadStoreForOrigin(
const url::Origin& origin) {
auto it = store_map_.find(origin);
if (it != store_map_.end())
return it->second.get();
// Create the store now, and serve the cached policy until the PolicyService
// sends updated values.
auto store = std::make_unique<ManagedConfigurationStore>(
backend_task_runner_, origin, GetStoreLocation(origin));
ManagedConfigurationStore* store_ptr = store.get();
store_map_[origin] = std::move(store);
return store_ptr;
}
std::unique_ptr<base::DictionaryValue>
ManagedConfigurationAPI::GetConfigurationOnBackend(
const url::Origin& origin,
const std::vector<std::string>& keys) {
// If there was no policy set for this origin, there is no reason to create
// or load a store.
if (!base::Contains(store_map_, origin))
return nullptr;
LeveldbValueStore::ReadResult result = store_map_[origin]->Get(keys);
if (!result.status().ok())
return nullptr;
auto dict = std::make_unique<base::DictionaryValue>();
dict->Swap(&result.settings());
return dict;
}
base::FilePath ManagedConfigurationAPI::GetStoreLocation(
const url::Origin& origin) {
return stores_path_.AppendASCII(GetOriginEncoded(origin));
}
void ManagedConfigurationAPI::UpdateStoredDataForOrigin(
const url::Origin& origin,
const std::string& configuration_url,
const std::string& configuration_hash) {
const std::string* last_hash_value =
profile_->GetPrefs()
->GetDictionary(prefs::kLastManagedConfigurationHashForOrigin)
->FindStringKey(GetOriginEncoded(origin));
// Nothing to be stored here, the hash value is the same.
if (last_hash_value && *last_hash_value == configuration_hash)
return;
if (configuration_url.empty()) {
PostStoreConfiguration(origin, base::DictionaryValue());
return;
}
// Check whether there is already a downloader.
if (downloaders_[origin]) {
// If it downloads the same data already, do nothing.
if (downloaders_[origin]->hash() == configuration_hash)
return;
// Cancel it otherwise.
downloaders_[origin].reset();
}
downloaders_[origin] =
std::make_unique<ManagedConfigurationDownloader>(configuration_hash);
downloaders_[origin]->Fetch(
configuration_url, base::BindOnce(&ManagedConfigurationAPI::DecodeData,
weak_ptr_factory_.GetWeakPtr(), origin,
configuration_hash));
}
void ManagedConfigurationAPI::DecodeData(const url::Origin& origin,
const std::string& url_hash,
std::unique_ptr<std::string> data) {
downloaders_[origin].reset();
if (!data)
return;
// First, we have to parse JSON file in an isolated sandbox so that we don't
// have to worry about potentially risky values.
data_decoder::DataDecoder::ParseJsonIsolated(
*data,
base::BindOnce(&ManagedConfigurationAPI::ProcessDecodedConfiguration,
weak_ptr_factory_.GetWeakPtr(), origin, url_hash));
}
void ManagedConfigurationAPI::ProcessDecodedConfiguration(
const url::Origin& origin,
const std::string& url_hash,
const data_decoder::DataDecoder::ValueOrError decoding_result) {
if (!decoding_result.value) {
VLOG(1) << "Could not fetch managed configuration for app with origin = "
<< origin.Serialize();
PostStoreConfiguration(origin, base::DictionaryValue());
return;
}
DictionaryPrefUpdate update(profile_->GetPrefs(),
prefs::kLastManagedConfigurationHashForOrigin);
update.Get()->SetStringKey(GetOriginEncoded(origin), url_hash);
// We need to transform each value into a string.
base::DictionaryValue result_dict;
for (const auto& item : decoding_result.value->DictItems()) {
std::string result;
JSONStringValueSerializer serializer(&result);
serializer.Serialize(item.second);
result_dict.SetString(item.first, result);
}
PostStoreConfiguration(origin, std::move(result_dict));
}
void ManagedConfigurationAPI::PostStoreConfiguration(
const url::Origin& origin,
base::DictionaryValue configuration) {
// Safe to use unretained here, since we own the task runner.
backend_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&ManagedConfigurationAPI::StoreConfigurationOnBackend,
base::Unretained(this), origin, std::move(configuration)));
}
void ManagedConfigurationAPI::StoreConfigurationOnBackend(
const url::Origin& origin,
base::DictionaryValue configuration) {
GetOrLoadStoreForOrigin(origin)->SetCurrentPolicy(configuration);
}
void ManagedConfigurationAPI::PromoteObservers() {
for (auto it = unmanaged_observers_.begin();
it != unmanaged_observers_.end();) {
if (CanHaveManagedStore((*it)->GetOrigin())) {
auto* observer = *it;
it = unmanaged_observers_.erase(it);
AddObserver(observer);
} else {
++it;
}
}
}