blob: 84139e1be3a665b1624fe095454ceb71b95aad5d [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/supervised_user/supervised_user_extensions_manager.h"
#include <optional>
#include <string>
#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/supervised_user/supervised_user_browser_utils.h"
#include "chrome/browser/supervised_user/supervised_user_extensions_metrics_recorder.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/supervised_user/core/browser/supervised_user_preferences.h"
#include "components/supervised_user/core/browser/supervised_user_service.h"
#include "components/supervised_user/core/common/features.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pref_names.h"
#include "ui/base/l10n/l10n_util.h"
namespace extensions {
namespace {
// These extensions are allowed for supervised users for internal development
// purposes.
constexpr char const* kAllowlistExtensionIds[] = {
"behllobkkfkfnphdnhnkndlbkcpglgmj" // Tast extension.
};
// Returns the set of extensions that are missing parent approval.
base::Value::Dict GetExtensionsMissingApproval(const PrefService& user_prefs) {
const base::Value::Dict& user_extensions_dict =
user_prefs.GetDict(pref_names::kExtensions);
const base::Value::Dict& approved_extensions_dict =
user_prefs.GetDict(prefs::kSupervisedUserApprovedExtensions);
base::Value::Dict unapproved_extensions_dict;
// Deduce which extensions are not parent-approved based on the
// corresponding preferences, as at the time of creation of
// `SupervisedUserExtensionsManager`, the extensions are not yet loaded in
// the registry.
for (auto extension_entry : user_extensions_dict) {
if (!approved_extensions_dict.contains(extension_entry.first)) {
unapproved_extensions_dict.Set(extension_entry.first, true);
}
}
return unapproved_extensions_dict;
}
} // namespace
SupervisedUserExtensionsManager::SupervisedUserExtensionsManager(
content::BrowserContext* context)
: context_(context),
extension_prefs_(ExtensionPrefs::Get(static_cast<Profile*>(context))),
extension_system_(ExtensionSystem::Get(static_cast<Profile*>(context))),
extension_registry_(
ExtensionRegistry::Get(static_cast<Profile*>(context))),
user_prefs_(static_cast<Profile*>(context)->GetPrefs()) {
registry_observation_.Observe(extension_registry_);
pref_change_registrar_.Init(user_prefs_);
pref_change_registrar_.Add(
prefs::kSupervisedUserId,
base::BindRepeating(&SupervisedUserExtensionsManager::
ActivateManagementPolicyAndUpdateRegistration,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kSupervisedUserApprovedExtensions,
base::BindRepeating(
&SupervisedUserExtensionsManager::RefreshApprovedExtensionsFromPrefs,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kSkipParentApprovalToInstallExtensions,
base::BindRepeating(&SupervisedUserExtensionsManager::
OnSkipParentApprovalToInstallExtensionsChanged,
base::Unretained(this)));
RefreshApprovedExtensionsFromPrefs();
ActivateManagementPolicyAndUpdateRegistration();
}
SupervisedUserExtensionsManager::~SupervisedUserExtensionsManager() {
pref_change_registrar_.RemoveAll();
extensions::ManagementPolicy* management_policy =
extension_system_->management_policy();
if (management_policy && is_active_policy_for_supervised_users_) {
management_policy->UnregisterProvider(this);
}
}
void SupervisedUserExtensionsManager::UpdateManagementPolicyRegistration() {
extensions::ManagementPolicy* management_policy =
extension_system_->management_policy();
if (management_policy) {
if (is_active_policy_for_supervised_users_) {
management_policy->RegisterProvider(this);
} else {
management_policy->UnregisterProvider(this);
}
// Re-check the policy to make sure any new settings get applied.
extension_system_->extension_service()->CheckManagementPolicy();
}
}
void SupervisedUserExtensionsManager::AddExtensionApproval(
const extensions::Extension& extension) {
if (!is_active_policy_for_supervised_users_) {
return;
}
if (!base::Contains(approved_extensions_set_, extension.id())) {
UpdateApprovedExtension(extension.id(), extension.VersionString(),
ApprovedExtensionChange::kAdd);
}
}
void SupervisedUserExtensionsManager::MaybeRecordPermissionsIncreaseMetrics(
const extensions::Extension& extension) {
if (!is_active_policy_for_supervised_users_) {
return;
}
if (extension_prefs_->DidExtensionEscalatePermissions(extension.id())) {
SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
kPermissionsIncreaseGranted);
}
}
void SupervisedUserExtensionsManager::RemoveExtensionApproval(
const extensions::Extension& extension) {
if (!is_active_policy_for_supervised_users_) {
return;
}
if (base::Contains(approved_extensions_set_, extension.id())) {
UpdateApprovedExtension(extension.id(), extension.VersionString(),
ApprovedExtensionChange::kRemove);
}
}
bool SupervisedUserExtensionsManager::IsExtensionAllowed(
const extensions::Extension& extension) const {
return GetExtensionState(extension) ==
SupervisedUserExtensionsManager::ExtensionState::ALLOWED;
}
bool SupervisedUserExtensionsManager::CanInstallExtensions() const {
supervised_user::SupervisedUserService* supervised_user_service =
SupervisedUserServiceFactory::GetForBrowserContext(context_);
bool has_custodian = supervised_user_service->GetCustodian() ||
supervised_user_service->GetSecondCustodian();
return has_custodian;
}
void SupervisedUserExtensionsManager::RecordExtensionEnablementUmaMetrics(
bool enabled) const {
if (!is_active_policy_for_supervised_users_) {
return;
}
auto state =
enabled
? SupervisedUserExtensionsMetricsRecorder::EnablementState::kEnabled
: SupervisedUserExtensionsMetricsRecorder::EnablementState::kDisabled;
SupervisedUserExtensionsMetricsRecorder::RecordEnablementUmaMetrics(state);
}
std::string SupervisedUserExtensionsManager::GetDebugPolicyProviderName()
const {
// Save the string space in official builds.
#if DCHECK_IS_ON()
return "Supervised User Service";
#else
base::ImmediateCrash();
#endif
}
bool SupervisedUserExtensionsManager::UserMayLoad(
const extensions::Extension* extension,
std::u16string* error) const {
ExtensionState result = GetExtensionState(*extension);
bool may_load = result != ExtensionState::BLOCKED;
if (!may_load && error) {
*error = GetExtensionsLockedMessage();
}
return may_load;
}
bool SupervisedUserExtensionsManager::MustRemainDisabled(
const extensions::Extension* extension,
extensions::disable_reason::DisableReason* reason) const {
ExtensionState state = GetExtensionState(*extension);
// Only extensions that require approval should be disabled.
// Blocked extensions should be not loaded at all, and are taken care of
// at UserMayLoad.
bool must_remain_disabled = state == ExtensionState::REQUIRE_APPROVAL;
if (!must_remain_disabled) {
return false;
}
if (reason) {
*reason = extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED;
}
return true;
}
void SupervisedUserExtensionsManager::OnExtensionInstalled(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
bool is_update) {
if (!is_update) {
if (!is_active_policy_for_supervised_users_) {
return;
}
// At the end of an extension installation under the mode where
// the child can skip parental approval, we grant the extension
// the parental approval.
// Applies to all installations (via the Webstore in the same
// client and for extensions received through sync).
const Profile* profile = Profile::FromBrowserContext(browser_context);
if (!supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
profile)) {
return;
}
CHECK(extension);
if (!base::Contains(approved_extensions_set_, extension->id())) {
AddExtensionApproval(*extension);
SupervisedUserExtensionsMetricsRecorder::
RecordImplicitParentApprovalGrantEntryPointEntryPointUmaMetrics(
SupervisedUserExtensionsMetricsRecorder::
ImplicitExtensionApprovalEntryPoint::
OnExtensionInstallationWithExtensionsSwitchEnabled);
}
}
// A change in extension state might be required upon extension update
// or upon granting a new approval.
ChangeExtensionStateIfNecessary(extension->id());
}
void SupervisedUserExtensionsManager::OnExtensionUninstalled(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UninstallReason reason) {
if (!is_active_policy_for_supervised_users_) {
return;
}
if (base::Contains(approved_extensions_set_, extension->id())) {
UpdateApprovedExtension(extension->id(), extension->VersionString(),
ApprovedExtensionChange::kRemove);
}
if (IsLocallyParentApprovedExtension(extension->id())) {
RemoveLocalParentalApproval(/*extension_ids=*/{extension->id()});
}
}
SupervisedUserExtensionsManager::ExtensionState
SupervisedUserExtensionsManager::GetExtensionState(
const extensions::Extension& extension) const {
bool was_installed_by_default = extension.was_installed_by_default();
#if BUILDFLAG(IS_CHROMEOS)
// On Chrome OS all external sources are controlled by us so it means that
// they are "default". Method was_installed_by_default returns false because
// extensions creation flags are ignored in case of default extensions with
// update URL(the flags aren't passed to OnExternalExtensionUpdateUrlFound).
// TODO(dpolukhin): remove this Chrome OS specific code as soon as creation
// flags are not ignored.
was_installed_by_default =
extensions::Manifest::IsExternalLocation(extension.location());
#endif
// Note: Component extensions are protected from modification/uninstallation
// anyway, so there's no need to enforce them again for supervised users.
// Also, leave policy-installed extensions alone - they have their own
// management; in particular we don't want to override the force-install list.
if (extensions::Manifest::IsComponentLocation(extension.location()) ||
extensions::Manifest::IsPolicyLocation(extension.location()) ||
extension.is_theme() || extension.is_shared_module() ||
was_installed_by_default) {
return SupervisedUserExtensionsManager::ExtensionState::ALLOWED;
}
if (base::Contains(kAllowlistExtensionIds, extension.id())) {
return SupervisedUserExtensionsManager::ExtensionState::ALLOWED;
}
if (ShouldBlockExtension(extension.id())) {
return SupervisedUserExtensionsManager::ExtensionState::BLOCKED;
}
if (base::Contains(approved_extensions_set_, extension.id())) {
return SupervisedUserExtensionsManager::ExtensionState::ALLOWED;
}
if (IsLocallyParentApprovedExtension(extension.id())) {
return SupervisedUserExtensionsManager::ExtensionState::ALLOWED;
}
return SupervisedUserExtensionsManager::ExtensionState::REQUIRE_APPROVAL;
}
void SupervisedUserExtensionsManager::RefreshApprovedExtensionsFromPrefs() {
// Keep track of currently approved extensions. We need to disable them if
// they are not in the approved set anymore.
std::set<std::string> extensions_to_be_checked(
std::move(approved_extensions_set_));
// The purpose here is to re-populate the approved_extensions_set_, which is
// used in GetExtensionState() to keep track of approved extensions.
approved_extensions_set_.clear();
// TODO(crbug.com/40685974): This dict is actually just a set. The extension
// version information stored in the values is unnecessary. It is only there
// for backwards compatibility. Remove the version information once sufficient
// users have migrated away from M83.
for (auto it :
user_prefs_->GetDict(prefs::kSupervisedUserApprovedExtensions)) {
approved_extensions_set_.insert(it.first);
extensions_to_be_checked.insert(it.first);
}
// If an extension goes through the parent approval flow in another client
// with extension parental controls in place, we remove it in this client from
// the locally parent-approval set.
RemoveLocalParentalApproval(approved_extensions_set_);
for (const auto& extension_id : extensions_to_be_checked) {
ChangeExtensionStateIfNecessary(extension_id);
}
}
void SupervisedUserExtensionsManager::SetActiveForSupervisedUsers() {
auto* profile = Profile::FromBrowserContext(context_);
is_active_policy_for_supervised_users_ =
profile && supervised_user::AreExtensionsPermissionsEnabled(profile);
}
void SupervisedUserExtensionsManager::
ActivateManagementPolicyAndUpdateRegistration() {
SetActiveForSupervisedUsers();
UpdateManagementPolicyRegistration();
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
MaybeMarkExtensionsLocallyParentApproved();
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}
// TODO(crbug.com/40685974): We don't need the extension version information.
// It's only included for backwards compatibility with previous versions of
// Chrome. Remove the version information once a sufficient number of users have
// migrated away from M83.
void SupervisedUserExtensionsManager::UpdateApprovedExtension(
const std::string& extension_id,
const std::string& version,
ApprovedExtensionChange type) {
ScopedDictPrefUpdate update(user_prefs_,
prefs::kSupervisedUserApprovedExtensions);
base::Value::Dict& approved_extensions = update.Get();
bool success = false;
const Profile* profile = Profile::FromBrowserContext(context_);
switch (type) {
case ApprovedExtensionChange::kAdd:
CHECK(!approved_extensions.FindString(extension_id));
approved_extensions.Set(extension_id, std::move(version));
SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
profile)
? SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
kApprovalGrantedByDefault
: SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
kApprovalGranted);
break;
case ApprovedExtensionChange::kRemove:
success = approved_extensions.Remove(extension_id);
CHECK(success);
SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
kApprovalRemoved);
break;
}
}
std::u16string SupervisedUserExtensionsManager::GetExtensionsLockedMessage()
const {
supervised_user::SupervisedUserService* supervised_user_service =
SupervisedUserServiceFactory::GetForBrowserContext(context_);
std::optional<supervised_user::Custodian> custodian =
supervised_user_service->GetCustodian();
return l10n_util::GetStringFUTF16(
IDS_EXTENSIONS_LOCKED_SUPERVISED_USER,
base::UTF8ToUTF16(custodian ? custodian->GetName() : ""));
}
void SupervisedUserExtensionsManager::ChangeExtensionStateIfNecessary(
const std::string& extension_id) {
// If the profile is not supervised, do nothing.
// TODO(crbug/1026900): SupervisedUserService should not be active if the
// profile is not even supervised during browser tests, i.e. this check
// shouldn't be needed.
if (!is_active_policy_for_supervised_users_) {
return;
}
const Extension* extension =
extension_registry_->GetInstalledExtension(extension_id);
// If the extension is not installed (yet), do nothing.
// Things will be handled after installation.
if (!extension) {
return;
}
auto* registrar = extensions::ExtensionRegistrar::Get(context_);
ExtensionState state = GetExtensionState(*extension);
switch (state) {
// BLOCKED extensions should be already disabled and we don't need to change
// their state here.
case ExtensionState::BLOCKED:
break;
case ExtensionState::REQUIRE_APPROVAL:
registrar->DisableExtension(
extension_id,
{extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED});
break;
case ExtensionState::ALLOWED:
extension_prefs_->RemoveDisableReason(
extension_id,
extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED);
// If not disabled for other reasons, enable it.
if (extension_prefs_->GetDisableReasons(extension_id).empty()) {
registrar->EnableExtension(extension_id);
}
break;
}
}
bool SupervisedUserExtensionsManager::ShouldBlockExtension(
const std::string& extension_id) const {
// Users can always install extensions.
return false;
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
void SupervisedUserExtensionsManager::
MaybeMarkExtensionsLocallyParentApproved() {
supervised_user::LocallyParentApprovedExtensionsMigrationState
migration_state = static_cast<
supervised_user::LocallyParentApprovedExtensionsMigrationState>(
user_prefs_->GetInteger(
prefs::kLocallyParentApprovedExtensionsMigrationState));
if (migration_state ==
supervised_user::LocallyParentApprovedExtensionsMigrationState::
kComplete) {
return;
}
if (supervised_user::IsSubjectToParentalControls(*user_prefs_)) {
// In the case of of a supervised user locally approve their extensions.
DoExtensionsMigrationToParentApproved();
}
// Always mark the migration done on feature release for the currently used
// profile. Applies to both for supervised and regular users. This way, if the
// profile is later Gellerized or if a supervised user takes over an existing
// unsupervised profile, their extensions will not be locally approved,
// instead they should remain in pending approval state.
user_prefs_->SetInteger(
prefs::kLocallyParentApprovedExtensionsMigrationState,
static_cast<int>(
supervised_user::LocallyParentApprovedExtensionsMigrationState::
kComplete));
}
void SupervisedUserExtensionsManager::DoExtensionsMigrationToParentApproved() {
base::Value::Dict unapproved_extensions_dict =
GetExtensionsMissingApproval(*user_prefs_);
user_prefs_->SetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions,
std::move(unapproved_extensions_dict));
auto& approved_extensions_dict = user_prefs_->GetDict(
prefs::kSupervisedUserLocallyParentApprovedExtensions);
for (auto extension_entry : approved_extensions_dict) {
if (extension_registry_->GetInstalledExtension(extension_entry.first)) {
ChangeExtensionStateIfNecessary(extension_entry.first);
}
SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
kLocalApprovalGranted);
}
base::UmaHistogramCounts1000(
kInitialLocallyApprovedExtensionCountWinLinuxMacHistogramName,
approved_extensions_dict.size());
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
bool SupervisedUserExtensionsManager::IsLocallyParentApprovedExtension(
const std::string& extension_id) const {
const base::Value::Dict& current_locally_approved_dict = user_prefs_->GetDict(
prefs::kSupervisedUserLocallyParentApprovedExtensions);
return base::Contains(current_locally_approved_dict, extension_id);
}
void SupervisedUserExtensionsManager::RemoveLocalParentalApproval(
const std::set<std::string>& extension_ids) {
base::Value::Dict locally_approved_extensions_dict =
user_prefs_
->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions)
.Clone();
for (const auto& extension_id : extension_ids) {
locally_approved_extensions_dict.Remove(extension_id);
}
user_prefs_->SetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions,
std::move(locally_approved_extensions_dict));
}
void SupervisedUserExtensionsManager::
OnSkipParentApprovalToInstallExtensionsChanged() {
const Profile* profile = Profile::FromBrowserContext(context_);
if (!is_active_policy_for_supervised_users_ ||
!supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
profile)) {
return;
}
auto unapproved_extensions_dict =
GetExtensionsMissingApproval(*user_prefs_.get());
int installed_extensions_approvals_count = 0;
for (auto extension_entry : unapproved_extensions_dict) {
const Extension* extension =
extension_registry_->GetInstalledExtension(extension_entry.first);
if (extension) {
ExtensionState state = GetExtensionState(*extension);
if (state == ExtensionState::REQUIRE_APPROVAL ||
(state == ExtensionState::ALLOWED &&
IsLocallyParentApprovedExtension(extension->id()))) {
AddExtensionApproval(*extension);
SupervisedUserExtensionsMetricsRecorder::
RecordImplicitParentApprovalGrantEntryPointEntryPointUmaMetrics(
SupervisedUserExtensionsMetricsRecorder::
ImplicitExtensionApprovalEntryPoint::
kOnExtensionsSwitchFlippedToEnabled);
installed_extensions_approvals_count += 1;
}
// If the extension id from the preferences has not been installed yet,
// the approval will be granted at the end of installation.
// See `OnExtensionInstalled`.
}
}
base::UmaHistogramCounts1000(
kExtensionApprovalsCountOnExtensionToggleHistogramName,
installed_extensions_approvals_count);
}
} // namespace extensions