// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/policy/model/policy_watcher_browser_agent.h"
#import <Foundation/Foundation.h>
#import "base/apple/backup_util.h"
#import "base/apple/foundation_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/path_service.h"
#import "base/run_loop.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/prefs/pref_service.h"
#import "components/sync/base/pref_names.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/policy/model/policy_watcher_browser_agent_observer.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/policy_change_commands.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_utils.h"
#import "ios/web/public/thread/web_task_traits.h"
NSString* kSyncDisabledAlertShownKey = @"SyncDisabledAlertShown";
BROWSER_USER_DATA_KEY_IMPL(PolicyWatcherBrowserAgent)
PolicyWatcherBrowserAgent::PolicyWatcherBrowserAgent(Browser* browser)
: browser_(browser) {
DCHECK(!browser->GetBrowserState()->IsOffTheRecord());
prefs_change_observer_.Init(GetApplicationContext()->GetLocalState());
browser_prefs_change_observer_.Init(browser->GetBrowserState()->GetPrefs());
}
PolicyWatcherBrowserAgent::~PolicyWatcherBrowserAgent() = default;
void PolicyWatcherBrowserAgent::SignInUIDismissed() {
// Do nothing if the sign out is still in progress.
if (sign_out_in_progress_)
return;
[handler_ showForceSignedOutPrompt];
}
void PolicyWatcherBrowserAgent::Initialize(id<PolicyChangeCommands> handler) {
DCHECK(!handler_);
DCHECK(handler);
handler_ = handler;
auth_service_ = AuthenticationServiceFactory::GetForBrowserState(
browser_->GetBrowserState());
DCHECK(auth_service_);
auth_service_observation_.Observe(auth_service_.get());
// BrowserSignin policy: start observing the kSigninAllowed pref. When the
// pref becomes false, send a UI command to sign the user out. This requires
// the given command dispatcher to be fully configured.
prefs_change_observer_.Add(
prefs::kBrowserSigninPolicy,
base::BindRepeating(
&PolicyWatcherBrowserAgent::ForceSignOutIfSigninDisabled,
base::Unretained(this)));
// Try to sign out in case the policy changed since last time. This should be
// done after the handler is set to make sure the UI can be displayed.
ForceSignOutIfSigninDisabled();
// TODO(crbug.com/40265119): Instead of directly accessing internal sync
// prefs, go through proper APIs (SyncService/SyncUserSettings).
browser_prefs_change_observer_.Add(
syncer::prefs::internal::kSyncManaged,
base::BindRepeating(
&PolicyWatcherBrowserAgent::ShowSyncDisabledPromptIfNeeded,
base::Unretained(this)));
// Try to show the alert in case the policy changed since last time.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&PolicyWatcherBrowserAgent::ShowSyncDisabledPromptIfNeeded,
weak_factory_.GetWeakPtr()));
browser_prefs_change_observer_.Add(
prefs::kAllowChromeDataInBackups,
base::BindRepeating(
&PolicyWatcherBrowserAgent::UpdateAppContainerBackupExclusion,
base::Unretained(this)));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&PolicyWatcherBrowserAgent::UpdateAppContainerBackupExclusion,
weak_factory_.GetWeakPtr()));
}
void PolicyWatcherBrowserAgent::ForceSignOutIfSigninDisabled() {
DCHECK(handler_);
DCHECK(auth_service_);
if ((auth_service_->GetServiceStatus() ==
AuthenticationService::ServiceStatus::SigninDisabledByPolicy)) {
if (auth_service_->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
sign_out_in_progress_ = true;
base::UmaHistogramBoolean("Enterprise.BrowserSigninIOS.SignedOutByPolicy",
true);
base::WeakPtr<PolicyWatcherBrowserAgent> weak_ptr =
weak_factory_.GetWeakPtr();
// Sign the user out, but keep synced data (bookmarks, passwords, etc)
// locally to be consistent with the policy's behavior on other platforms.
auth_service_->SignOut(signin_metrics::ProfileSignout::kPrefChanged,
/*force_clear_browsing_data=*/false, ^{
if (weak_ptr) {
weak_ptr->OnSignOutComplete();
}
});
}
for (auto& observer : observers_) {
observer.OnSignInDisallowed(this);
}
}
}
void PolicyWatcherBrowserAgent::ShowSyncDisabledPromptIfNeeded() {
NSUserDefaults* standard_defaults = [NSUserDefaults standardUserDefaults];
BOOL syncDisabledAlertShown =
[standard_defaults boolForKey:kSyncDisabledAlertShownKey];
// TODO(crbug.com/40265119): Instead of directly accessing internal sync
// prefs, go through proper APIs (SyncService/SyncUserSettings).
BOOL isSyncDisabledByAdministrator =
browser_->GetBrowserState()->GetPrefs()->GetBoolean(
syncer::prefs::internal::kSyncManaged);
if (!syncDisabledAlertShown && isSyncDisabledByAdministrator) {
SceneState* scene_state = browser_->GetSceneState();
BOOL scene_is_active =
scene_state.activationLevel >= SceneActivationLevelForegroundActive;
if (scene_is_active) {
[handler_ showSyncDisabledPrompt];
// Will never trigger again unless policy changes.
[standard_defaults setBool:YES forKey:kSyncDisabledAlertShownKey];
}
} else if (syncDisabledAlertShown && !isSyncDisabledByAdministrator) {
// Will trigger again, if policy is turned back on.
[standard_defaults setBool:NO forKey:kSyncDisabledAlertShownKey];
}
}
void PolicyWatcherBrowserAgent::UpdateAppContainerBackupExclusion() {
bool backup_allowed = browser_->GetBrowserState()->GetPrefs()->GetBoolean(
prefs::kAllowChromeDataInBackups);
// TODO(crbug.com/40826035): If multiple profiles are supported on iOS, update
// this logic to work with multiple profiles having possibly-possibly
// conflicting preference values.
base::FilePath storage_dir = base::apple::GetUserLibraryPath();
if (backup_allowed) {
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(base::IgnoreResult(&base::apple::ClearBackupExclusion),
std::move(storage_dir)));
} else {
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(base::IgnoreResult(&base::apple::SetBackupExclusion),
std::move(storage_dir)));
}
}
void PolicyWatcherBrowserAgent::AddObserver(
PolicyWatcherBrowserAgentObserver* observer) {
observers_.AddObserver(observer);
}
void PolicyWatcherBrowserAgent::RemoveObserver(
PolicyWatcherBrowserAgentObserver* observer) {
observers_.RemoveObserver(observer);
}
void PolicyWatcherBrowserAgent::OnSignOutComplete() {
SceneState* scene_state = browser_->GetSceneState();
sign_out_in_progress_ = false;
BOOL scene_is_active =
scene_state.activationLevel >= SceneActivationLevelForegroundActive;
if (scene_is_active) {
// Try to show the signout prompt in all cases: if there is a sign
// in in progress, the UI will prevent the prompt from showing.
[handler_ showForceSignedOutPrompt];
} else {
scene_state.appState.shouldShowForceSignOutPrompt = YES;
}
}
void PolicyWatcherBrowserAgent::OnPrimaryAccountRestricted() {
[handler_ showRestrictAccountSignedOutPrompt];
}