| // Copyright 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 "chrome/browser/component_updater/supervised_user_whitelist_installer.h" |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/location.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/path_service.h" |
| #include "base/prefs/pref_registry_simple.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/prefs/scoped_user_pref_update.h" |
| #include "base/scoped_observer.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_util.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "chrome/browser/profiles/profile_info_cache.h" |
| #include "chrome/browser/profiles/profile_info_cache_observer.h" |
| #include "chrome/browser/supervised_user/supervised_user_whitelist_service.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/component_updater/component_updater_paths.h" |
| #include "components/component_updater/component_updater_service.h" |
| #include "components/component_updater/default_component_installer.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/safe_json/json_sanitizer.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| namespace component_updater { |
| |
| namespace { |
| |
| const char kSanitizedWhitelistExtension[] = ".json"; |
| |
| const char kWhitelistedContent[] = "whitelisted_content"; |
| const char kSites[] = "sites"; |
| |
| const char kClients[] = "clients"; |
| const char kName[] = "name"; |
| |
| // These are copies of extensions::manifest_keys::kName and kShortName. They |
| // are duplicated here because we mustn't depend on code from extensions/ |
| // (since it's not built on Android). |
| const char kExtensionName[] = "name"; |
| const char kExtensionShortName[] = "short_name"; |
| |
| base::string16 GetWhitelistTitle(const base::DictionaryValue& manifest) { |
| base::string16 title; |
| if (!manifest.GetString(kExtensionShortName, &title)) |
| manifest.GetString(kExtensionName, &title); |
| return title; |
| } |
| |
| base::FilePath GetRawWhitelistPath(const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) { |
| const base::DictionaryValue* whitelist_dict = nullptr; |
| if (!manifest.GetDictionary(kWhitelistedContent, &whitelist_dict)) |
| return base::FilePath(); |
| |
| base::FilePath::StringType whitelist_file; |
| if (!whitelist_dict->GetString(kSites, &whitelist_file)) |
| return base::FilePath(); |
| |
| return install_dir.Append(whitelist_file); |
| } |
| |
| base::FilePath GetSanitizedWhitelistPath(const std::string& crx_id) { |
| base::FilePath base_dir; |
| PathService::Get(chrome::DIR_SUPERVISED_USER_INSTALLED_WHITELISTS, &base_dir); |
| return base_dir.empty() |
| ? base::FilePath() |
| : base_dir.AppendASCII(crx_id + kSanitizedWhitelistExtension); |
| } |
| |
| void RecordUncleanUninstall() { |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, FROM_HERE, |
| base::Bind( |
| &base::RecordAction, |
| base::UserMetricsAction("ManagedUsers_Whitelist_UncleanUninstall"))); |
| } |
| |
| void OnWhitelistSanitizationError(const base::FilePath& whitelist, |
| const std::string& error) { |
| LOG(WARNING) << "Invalid whitelist " << whitelist.value() << ": " << error; |
| } |
| |
| void OnWhitelistSanitizationResult( |
| const std::string& crx_id, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner, |
| const base::Closure& callback, |
| const std::string& result) { |
| const base::FilePath sanitized_whitelist_path = |
| GetSanitizedWhitelistPath(crx_id); |
| const base::FilePath install_directory = sanitized_whitelist_path.DirName(); |
| if (!base::DirectoryExists(install_directory)) { |
| if (!base::CreateDirectory(install_directory)) { |
| PLOG(ERROR) << "Could't create directory " << install_directory.value(); |
| return; |
| } |
| } |
| |
| const int size = result.size(); |
| if (base::WriteFile(sanitized_whitelist_path, result.data(), size) != size) { |
| PLOG(ERROR) << "Couldn't write file " << sanitized_whitelist_path.value(); |
| return; |
| } |
| task_runner->PostTask(FROM_HERE, callback); |
| } |
| |
| void CheckForSanitizedWhitelistOnTaskRunner( |
| const std::string& crx_id, |
| const base::FilePath& whitelist_path, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner, |
| const base::Closure& callback) { |
| if (base::PathExists(GetSanitizedWhitelistPath(crx_id))) { |
| task_runner->PostTask(FROM_HERE, callback); |
| return; |
| } |
| |
| std::string unsafe_json; |
| if (!base::ReadFileToString(whitelist_path, &unsafe_json)) { |
| PLOG(ERROR) << "Couldn't read file " << whitelist_path.value(); |
| return; |
| } |
| |
| safe_json::JsonSanitizer::Sanitize( |
| unsafe_json, |
| base::Bind(&OnWhitelistSanitizationResult, crx_id, task_runner, callback), |
| base::Bind(&OnWhitelistSanitizationError, whitelist_path)); |
| } |
| |
| void RemoveUnregisteredWhitelistsOnTaskRunner( |
| const std::set<std::string>& registered_whitelists) { |
| base::FilePath base_dir; |
| PathService::Get(DIR_SUPERVISED_USER_WHITELISTS, &base_dir); |
| if (!base_dir.empty()) { |
| base::FileEnumerator file_enumerator(base_dir, false, |
| base::FileEnumerator::DIRECTORIES); |
| for (base::FilePath path = file_enumerator.Next(); !path.value().empty(); |
| path = file_enumerator.Next()) { |
| const std::string crx_id = path.BaseName().MaybeAsASCII(); |
| |
| // Ignore folders that don't have valid CRX ID names. These folders are |
| // not managed by the component installer, so do not try to remove them. |
| if (!crx_file::id_util::IdIsValid(crx_id)) |
| continue; |
| |
| // Ignore folders that correspond to registered whitelists. |
| if (registered_whitelists.count(crx_id) > 0) |
| continue; |
| |
| RecordUncleanUninstall(); |
| |
| if (!base::DeleteFile(path, true)) |
| DPLOG(ERROR) << "Couldn't delete " << path.value(); |
| } |
| } |
| |
| PathService::Get(chrome::DIR_SUPERVISED_USER_INSTALLED_WHITELISTS, &base_dir); |
| if (!base_dir.empty()) { |
| base::FilePath pattern(FILE_PATH_LITERAL("*")); |
| pattern = pattern.AppendASCII(kSanitizedWhitelistExtension); |
| base::FileEnumerator file_enumerator( |
| base_dir, false, base::FileEnumerator::FILES, pattern.value()); |
| for (base::FilePath path = file_enumerator.Next(); !path.value().empty(); |
| path = file_enumerator.Next()) { |
| // Ignore files that don't have valid CRX ID names. These files are not |
| // managed by the component installer, so do not try to remove them. |
| const std::string filename = path.BaseName().MaybeAsASCII(); |
| DCHECK(base::EndsWith(filename, kSanitizedWhitelistExtension, |
| base::CompareCase::SENSITIVE)); |
| |
| const std::string crx_id = filename.substr( |
| filename.size() - strlen(kSanitizedWhitelistExtension)); |
| |
| if (!crx_file::id_util::IdIsValid(crx_id)) |
| continue; |
| |
| // Ignore files that correspond to registered whitelists. |
| if (registered_whitelists.count(crx_id) > 0) |
| continue; |
| |
| RecordUncleanUninstall(); |
| |
| if (!base::DeleteFile(path, true)) |
| DPLOG(ERROR) << "Couldn't delete " << path.value(); |
| } |
| } |
| } |
| |
| class SupervisedUserWhitelistComponentInstallerTraits |
| : public ComponentInstallerTraits { |
| public: |
| using RawWhitelistReadyCallback = |
| base::Callback<void(const base::string16&, const base::FilePath&)>; |
| |
| SupervisedUserWhitelistComponentInstallerTraits( |
| const std::string& crx_id, |
| const std::string& name, |
| const RawWhitelistReadyCallback& callback) |
| : crx_id_(crx_id), name_(name), callback_(callback) {} |
| ~SupervisedUserWhitelistComponentInstallerTraits() override {} |
| |
| private: |
| // ComponentInstallerTraits overrides: |
| bool VerifyInstallation(const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) const override; |
| bool CanAutoUpdate() const override; |
| bool OnCustomInstall(const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) override; |
| void ComponentReady(const base::Version& version, |
| const base::FilePath& install_dir, |
| scoped_ptr<base::DictionaryValue> manifest) override; |
| base::FilePath GetBaseDirectory() const override; |
| void GetHash(std::vector<uint8_t>* hash) const override; |
| std::string GetName() const override; |
| |
| std::string crx_id_; |
| std::string name_; |
| RawWhitelistReadyCallback callback_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SupervisedUserWhitelistComponentInstallerTraits); |
| }; |
| |
| bool SupervisedUserWhitelistComponentInstallerTraits::VerifyInstallation( |
| const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) const { |
| // Check whether the whitelist exists at the path specified by the manifest. |
| // This does not check whether the whitelist is wellformed. |
| return base::PathExists(GetRawWhitelistPath(manifest, install_dir)); |
| } |
| |
| bool SupervisedUserWhitelistComponentInstallerTraits::CanAutoUpdate() const { |
| return true; |
| } |
| |
| bool SupervisedUserWhitelistComponentInstallerTraits::OnCustomInstall( |
| const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) { |
| // Delete the existing sanitized whitelist. |
| return base::DeleteFile(GetSanitizedWhitelistPath(crx_id_), false); |
| } |
| |
| void SupervisedUserWhitelistComponentInstallerTraits::ComponentReady( |
| const base::Version& version, |
| const base::FilePath& install_dir, |
| scoped_ptr<base::DictionaryValue> manifest) { |
| // TODO(treib): Before getting the title, we should localize the manifest |
| // using extension_l10n_util::LocalizeExtension, but that doesn't exist on |
| // Android. crbug.com/558387 |
| callback_.Run(GetWhitelistTitle(*manifest), |
| GetRawWhitelistPath(*manifest, install_dir)); |
| } |
| |
| base::FilePath |
| SupervisedUserWhitelistComponentInstallerTraits::GetBaseDirectory() const { |
| base::FilePath whitelist_directory; |
| PathService::Get(DIR_SUPERVISED_USER_WHITELISTS, &whitelist_directory); |
| return whitelist_directory.AppendASCII(crx_id_); |
| } |
| |
| void SupervisedUserWhitelistComponentInstallerTraits::GetHash( |
| std::vector<uint8_t>* hash) const { |
| *hash = SupervisedUserWhitelistInstaller::GetHashFromCrxId(crx_id_); |
| } |
| |
| std::string SupervisedUserWhitelistComponentInstallerTraits::GetName() const { |
| return name_; |
| } |
| |
| class SupervisedUserWhitelistInstallerImpl |
| : public SupervisedUserWhitelistInstaller, |
| public ProfileInfoCacheObserver { |
| public: |
| SupervisedUserWhitelistInstallerImpl(ComponentUpdateService* cus, |
| ProfileInfoCache* profile_info_cache, |
| PrefService* local_state); |
| ~SupervisedUserWhitelistInstallerImpl() override {} |
| |
| private: |
| void RegisterComponent(const std::string& crx_id, |
| const std::string& name, |
| const base::Closure& callback); |
| void RegisterNewComponent(const std::string& crx_id, const std::string& name); |
| bool UnregisterWhitelistInternal(base::DictionaryValue* pref_dict, |
| const std::string& client_id, |
| const std::string& crx_id); |
| |
| void OnRawWhitelistReady(const std::string& crx_id, |
| const base::string16& title, |
| const base::FilePath& whitelist_path); |
| void OnSanitizedWhitelistReady(const std::string& crx_id, |
| const base::string16& title); |
| |
| // SupervisedUserWhitelistInstaller overrides: |
| void RegisterComponents() override; |
| void Subscribe(const WhitelistReadyCallback& callback) override; |
| void RegisterWhitelist(const std::string& client_id, |
| const std::string& crx_id, |
| const std::string& name) override; |
| void UnregisterWhitelist(const std::string& client_id, |
| const std::string& crx_id) override; |
| |
| // ProfileInfoCacheObserver overrides: |
| void OnProfileWillBeRemoved(const base::FilePath& profile_path) override; |
| |
| ComponentUpdateService* cus_; |
| PrefService* local_state_; |
| |
| std::vector<WhitelistReadyCallback> callbacks_; |
| |
| ScopedObserver<ProfileInfoCache, ProfileInfoCacheObserver> observer_; |
| |
| base::WeakPtrFactory<SupervisedUserWhitelistInstallerImpl> weak_ptr_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SupervisedUserWhitelistInstallerImpl); |
| }; |
| |
| SupervisedUserWhitelistInstallerImpl::SupervisedUserWhitelistInstallerImpl( |
| ComponentUpdateService* cus, |
| ProfileInfoCache* profile_info_cache, |
| PrefService* local_state) |
| : cus_(cus), |
| local_state_(local_state), |
| observer_(this), |
| weak_ptr_factory_(this) { |
| DCHECK(cus); |
| DCHECK(local_state); |
| // In unit tests, the profile info cache can be null. |
| if (profile_info_cache) |
| observer_.Add(profile_info_cache); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::RegisterComponent( |
| const std::string& crx_id, |
| const std::string& name, |
| const base::Closure& callback) { |
| scoped_ptr<ComponentInstallerTraits> traits( |
| new SupervisedUserWhitelistComponentInstallerTraits( |
| crx_id, name, |
| base::Bind(&SupervisedUserWhitelistInstallerImpl::OnRawWhitelistReady, |
| weak_ptr_factory_.GetWeakPtr(), crx_id))); |
| scoped_refptr<DefaultComponentInstaller> installer( |
| new DefaultComponentInstaller(traits.Pass())); |
| installer->Register(cus_, callback); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::RegisterNewComponent( |
| const std::string& crx_id, |
| const std::string& name) { |
| RegisterComponent( |
| crx_id, name, |
| base::Bind(&SupervisedUserWhitelistInstaller::TriggerComponentUpdate, |
| &cus_->GetOnDemandUpdater(), crx_id)); |
| } |
| |
| bool SupervisedUserWhitelistInstallerImpl::UnregisterWhitelistInternal( |
| base::DictionaryValue* pref_dict, |
| const std::string& client_id, |
| const std::string& crx_id) { |
| base::DictionaryValue* whitelist_dict = nullptr; |
| bool success = |
| pref_dict->GetDictionaryWithoutPathExpansion(crx_id, &whitelist_dict); |
| DCHECK(success); |
| base::ListValue* clients = nullptr; |
| success = whitelist_dict->GetList(kClients, &clients); |
| |
| const bool removed = clients->Remove(base::StringValue(client_id), nullptr); |
| |
| if (!clients->empty()) |
| return removed; |
| |
| pref_dict->RemoveWithoutPathExpansion(crx_id, nullptr); |
| bool result = cus_->UnregisterComponent(crx_id); |
| DCHECK(result); |
| |
| result = base::DeleteFile(GetSanitizedWhitelistPath(crx_id), false); |
| DCHECK(result); |
| |
| return removed; |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::OnRawWhitelistReady( |
| const std::string& crx_id, |
| const base::string16& title, |
| const base::FilePath& whitelist_path) { |
| cus_->GetSequencedTaskRunner()->PostTask( |
| FROM_HERE, |
| base::Bind( |
| &CheckForSanitizedWhitelistOnTaskRunner, crx_id, whitelist_path, |
| base::ThreadTaskRunnerHandle::Get(), |
| base::Bind( |
| &SupervisedUserWhitelistInstallerImpl::OnSanitizedWhitelistReady, |
| weak_ptr_factory_.GetWeakPtr(), crx_id, title))); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::OnSanitizedWhitelistReady( |
| const std::string& crx_id, |
| const base::string16& title) { |
| for (const WhitelistReadyCallback& callback : callbacks_) |
| callback.Run(crx_id, title, GetSanitizedWhitelistPath(crx_id)); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::RegisterComponents() { |
| const std::map<std::string, std::string> command_line_whitelists = |
| SupervisedUserWhitelistService::GetWhitelistsFromCommandLine(); |
| |
| std::set<std::string> registered_whitelists; |
| std::set<std::string> stale_whitelists; |
| DictionaryPrefUpdate update(local_state_, |
| prefs::kRegisteredSupervisedUserWhitelists); |
| base::DictionaryValue* whitelists = update.Get(); |
| for (base::DictionaryValue::Iterator it(*whitelists); !it.IsAtEnd(); |
| it.Advance()) { |
| const base::DictionaryValue* dict = nullptr; |
| it.value().GetAsDictionary(&dict); |
| |
| const std::string& id = it.key(); |
| |
| // Skip whitelists with no clients. This can happen when a whitelist was |
| // previously registered on the command line but isn't anymore. |
| const base::ListValue* clients = nullptr; |
| if ((!dict->GetList(kClients, &clients) || clients->empty()) && |
| command_line_whitelists.count(id) == 0) { |
| stale_whitelists.insert(id); |
| continue; |
| } |
| |
| std::string name; |
| bool result = dict->GetString(kName, &name); |
| DCHECK(result); |
| RegisterComponent(id, name, base::Closure()); |
| |
| registered_whitelists.insert(id); |
| } |
| |
| // Clean up stale whitelists as determined above. |
| for (const std::string& id : stale_whitelists) |
| whitelists->RemoveWithoutPathExpansion(id, nullptr); |
| |
| cus_->GetSequencedTaskRunner()->PostTask( |
| FROM_HERE, base::Bind(&RemoveUnregisteredWhitelistsOnTaskRunner, |
| registered_whitelists)); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::Subscribe( |
| const WhitelistReadyCallback& callback) { |
| return callbacks_.push_back(callback); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::RegisterWhitelist( |
| const std::string& client_id, |
| const std::string& crx_id, |
| const std::string& name) { |
| DictionaryPrefUpdate update(local_state_, |
| prefs::kRegisteredSupervisedUserWhitelists); |
| base::DictionaryValue* pref_dict = update.Get(); |
| base::DictionaryValue* whitelist_dict = nullptr; |
| const bool newly_added = |
| !pref_dict->GetDictionaryWithoutPathExpansion(crx_id, &whitelist_dict); |
| if (newly_added) { |
| whitelist_dict = new base::DictionaryValue; |
| whitelist_dict->SetString(kName, name); |
| pref_dict->SetWithoutPathExpansion(crx_id, whitelist_dict); |
| } |
| |
| if (!client_id.empty()) { |
| base::ListValue* clients = nullptr; |
| if (!whitelist_dict->GetList(kClients, &clients)) { |
| DCHECK(newly_added); |
| clients = new base::ListValue; |
| whitelist_dict->Set(kClients, clients); |
| } |
| bool success = |
| clients->AppendIfNotPresent(new base::StringValue(client_id)); |
| DCHECK(success); |
| } |
| |
| if (!newly_added) { |
| // Sanity-check that the stored name is equal to the name passed in. |
| // In release builds this is a no-op. |
| std::string stored_name; |
| DCHECK(whitelist_dict->GetString(kName, &stored_name)); |
| DCHECK_EQ(stored_name, name); |
| return; |
| } |
| |
| RegisterNewComponent(crx_id, name); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::UnregisterWhitelist( |
| const std::string& client_id, |
| const std::string& crx_id) { |
| DictionaryPrefUpdate update(local_state_, |
| prefs::kRegisteredSupervisedUserWhitelists); |
| bool removed = UnregisterWhitelistInternal(update.Get(), client_id, crx_id); |
| DCHECK(removed); |
| } |
| |
| void SupervisedUserWhitelistInstallerImpl::OnProfileWillBeRemoved( |
| const base::FilePath& profile_path) { |
| std::string client_id = ClientIdForProfilePath(profile_path); |
| |
| // Go through all registered whitelists and possibly unregister them for this |
| // client. |
| DictionaryPrefUpdate update(local_state_, |
| prefs::kRegisteredSupervisedUserWhitelists); |
| base::DictionaryValue* pref_dict = update.Get(); |
| for (base::DictionaryValue::Iterator it(*pref_dict); !it.IsAtEnd(); |
| it.Advance()) { |
| UnregisterWhitelistInternal(pref_dict, client_id, it.key()); |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| scoped_ptr<SupervisedUserWhitelistInstaller> |
| SupervisedUserWhitelistInstaller::Create(ComponentUpdateService* cus, |
| ProfileInfoCache* profile_info_cache, |
| PrefService* local_state) { |
| return make_scoped_ptr(new SupervisedUserWhitelistInstallerImpl( |
| cus, profile_info_cache, local_state)); |
| } |
| |
| // static |
| void SupervisedUserWhitelistInstaller::RegisterPrefs( |
| PrefRegistrySimple* registry) { |
| registry->RegisterDictionaryPref(prefs::kRegisteredSupervisedUserWhitelists); |
| } |
| |
| // static |
| std::string SupervisedUserWhitelistInstaller::ClientIdForProfilePath( |
| const base::FilePath& profile_path) { |
| // See ProfileInfoCache::CacheKeyFromProfilePath(). |
| return profile_path.BaseName().MaybeAsASCII(); |
| } |
| |
| // static |
| std::vector<uint8_t> SupervisedUserWhitelistInstaller::GetHashFromCrxId( |
| const std::string& crx_id) { |
| DCHECK(crx_file::id_util::IdIsValid(crx_id)); |
| |
| std::vector<uint8_t> hash; |
| uint8_t byte = 0; |
| for (size_t i = 0; i < crx_id.size(); ++i) { |
| // Uppercase characters in IDs are technically legal. |
| int val = base::ToLowerASCII(crx_id[i]) - 'a'; |
| DCHECK_GE(val, 0); |
| DCHECK_LT(val, 16); |
| if (i % 2 == 0) { |
| byte = val; |
| } else { |
| hash.push_back(16 * byte + val); |
| byte = 0; |
| } |
| } |
| return hash; |
| } |
| |
| // static |
| void SupervisedUserWhitelistInstaller::TriggerComponentUpdate( |
| OnDemandUpdater* updater, |
| const std::string& crx_id) { |
| const bool result = updater->OnDemandUpdate(crx_id); |
| DCHECK(result); |
| } |
| |
| } // namespace component_updater |