blob: 0a250569258ee4212c1090330b3f6d5ac23e6984 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
#include <memory>
#include <optional>
#include "base/cancelable_callback.h"
#include "base/feature_list.h"
#include "base/functional/callback_forward.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
#include "chrome/browser/signin/web_signin_interceptor.h"
#include "chrome/browser/ui/webui/signin/signin_utils.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/policy/core/browser/signin/profile_separation_policies.h"
#include "components/search_engines/template_url_data.h"
#include "components/signin/public/base/signin_metrics.h"
#include "components/signin/public/base/signin_prefs.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "google_apis/gaia/core_account_id.h"
#include "third_party/skia/include/core/SkColor.h"
namespace base {
class FilePath;
}
namespace content {
class WebContents;
}
namespace policy {
class UserCloudSigninRestrictionPolicyFetcher;
}
namespace user_prefs {
class PrefRegistrySyncable;
}
struct AccountInfo;
class DiceSignedInProfileCreator;
class DiceInterceptedSessionStartupHelper;
class Profile;
class ProfileAttributesEntry;
class ProfileAttributesStorage;
// This enum gets the result of `MaybeShouldShowChromeSigninBubble()`, which
// could be `ShouldShow` or `ShouldNotShow`. When the result is `ShouldNotShow`
// the reason is also added to differentiate the cases of not showing the
// bubble. These values are persisted to logs. Entries should not be renumbered
// and numeric values should never be reused.
enum class ShouldShowChromeSigninBubbleWithReason {
// The bubble should be shown.
kShouldShow = 0,
// The bubble should not be shown: multiple reasons listed below with order of
// priority.
// Deprecated: kShouldNotShowMaxShownCountReached = 1,
kShouldNotShowAlreadySignedIn = 2,
// Deprecated: kShouldNotShowSecondaryAccount = 3,
kShouldNotShowUnknownAccessPoint = 4,
kShouldNotShowNotFromWebSignin = 5,
kShouldNotShowUserChoice = 6,
kMaxValue = kShouldNotShowUserChoice,
};
// Supervision state of the user who is shown the sign-in intercept bubble.
// These values are logged to UMA. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(SinginInterceptSupervisionState)
enum class SinginInterceptSupervisionState {
kRegularUser = 0,
kSupervisedUser = 1,
kUnknownSupervision = 2,
kMaxValue = kUnknownSupervision,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/signin/enums.xml:SinginInterceptSupervisionState)
// Called after web signed in, after a successful token exchange through Dice.
// The DiceWebSigninInterceptor may offer the user to create a new profile or
// switch to another existing profile.
//
// Implementation notes: here is how an entire interception flow work for the
// enterprise or multi-user case:
// * MaybeInterceptWebSignin() is called when the new signin happens.
// * Wait until the account info is downloaded.
// * Interception UI is shown by the delegate. Keep a handle on the bubble.
// * If the user approved, a new profile is created and the token is moved from
// this profile to the new profile, using DiceSignedInProfileCreator.
// * At this point, the flow ends in this profile, and continues in the new
// profile using DiceInterceptedSessionStartupHelper to add the account.
// * When the account is available on the web in the new profile:
// - A new browser window is created for the new profile,
// - The tab is moved to the new profile,
// - The interception bubble is closed by deleting the handle,
// - The profile customization bubble is shown.
class DiceWebSigninInterceptor : public KeyedService,
public signin::IdentityManager::Observer {
public:
DiceWebSigninInterceptor(
Profile* profile,
std::unique_ptr<WebSigninInterceptor::Delegate> delegate);
~DiceWebSigninInterceptor() override;
DiceWebSigninInterceptor(const DiceWebSigninInterceptor&) = delete;
DiceWebSigninInterceptor& operator=(const DiceWebSigninInterceptor&) = delete;
static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
// Called when an account has been added in Chrome from the web (using the
// DICE protocol).
// `web_contents` is the tab where the signin event happened. It must belong
// to the profile associated with this service. It may be nullptr if the tab
// was closed.
// `is_new_account` is true if the account was not already in Chrome (i.e.
// this is not a reauth).
// `is_sync_signin` is true if the user is signing in with the intent of
// enabling sync for that account.
// Virtual for testing.
virtual void MaybeInterceptWebSignin(content::WebContents* web_contents,
CoreAccountId account_id,
signin_metrics::AccessPoint access_point,
bool is_new_account,
bool is_sync_signin);
// Called after the new profile was created during a signin interception.
// The token has been moved to the new profile, but the account is not yet in
// the cookies.
// `intercepted_contents` may be null if the tab was already closed.
// The intercepted web contents belong to the source profile (which is not the
// profile attached to this service).
void CreateBrowserAfterSigninInterception(
CoreAccountId account_id,
content::WebContents* intercepted_contents,
std::unique_ptr<ScopedWebSigninInterceptionBubbleHandle> bubble_handle,
bool is_new_profile,
WebSigninInterceptor::SigninInterceptionType interception_type);
// Returns the outcome of the interception heuristic.
// If the outcome is kInterceptProfileSwitch, the target profile is returned
// in |entry|.
// In some cases the outcome cannot be fully computed synchronously, when this
// happens, the signin interception is highly likely (but not guaranteed).
// `gaia_id` is optional as some usages may not have the information yet. It
// is currently only mandatory for the checks of the Chrome Signin bubble.
std::optional<SigninInterceptionHeuristicOutcome> GetHeuristicOutcome(
bool is_new_account,
bool is_sync_signin,
const std::string& email,
const GaiaId& gaia_id = GaiaId(),
bool update_state = false,
const ProfileAttributesEntry** entry = nullptr) const;
// Returns true if the interception is in progress (running the heuristic or
// showing on screen).
bool is_interception_in_progress() const {
return state_->is_interception_in_progress_;
}
content::WebContents* web_contents() const {
return state_->web_contents_.get();
}
const std::optional<policy::ProfileSeparationPolicies>
intercepted_account_profile_separation_policies() const {
return state_->intercepted_account_profile_separation_policies_;
}
bool managed_profile_creation_required_by_policy() const;
AccountInfo intercepted_account_info() const;
void SetInterceptedAccountProfileSeparationPoliciesForTesting(
std::optional<policy::ProfileSeparationPolicies> value) {
intercepted_account_profile_separation_policies_response_for_testing_ =
std::move(value);
}
static base::TimeDelta GetTimeSinceLastChromeSigninDeclineForTesting(
const SigninPrefs& signin_prefs,
const GaiaId& gaia_id);
// KeyedService:
void Shutdown() override;
private:
friend class DiceWebSigninInterceptorWithChromeSigninHelpersBrowserTest;
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldShowProfileSwitchBubble);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
NoBubbleWithSingleAccount);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldShowEnterpriseBubble);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldShowEnterpriseBubbleWithoutUPA);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldShowMultiUserBubble);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldShowMultiUserBubbleNoPrimaryAccount);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, PersistentHash);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldEnforceEnterpriseProfileSeparation);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldEnforceEnterpriseProfileSeparationWithoutUPA);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldEnforceEnterpriseProfileSeparationReauth);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
EnforceManagedAccountAsPrimary);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ShouldEnforceEnterpriseProfileSeparationReauth);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
ForcedEnterpriseInterceptionTestAccountLevelPolicy);
FRIEND_TEST_ALL_PREFIXES(
DiceWebSigninInterceptorTest,
ForcedEnterpriseInterceptionTestNoForcedInterception);
FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, StateResetTest);
FRIEND_TEST_ALL_PREFIXES(ManagedProfileRequiredNavigationThrottleTest,
CancelsWithInterstitialWhenForcedInterception);
// Profile presets that will be passed from the previous profile to the newly
// created one during sign in intercept.
struct ProfilePresets {
// This constructor is needed to be able to set just the profile theme until
// the `SearchEngineChoice` feature is enabled.
explicit ProfilePresets(SkColor profile_color);
~ProfilePresets();
ProfilePresets(ProfilePresets&&) = default;
ProfilePresets& operator=(ProfilePresets&&) = default;
ProfilePresets(const ProfilePresets&) = delete;
ProfilePresets& operator=(ProfilePresets&) = delete;
SkColor profile_color = SK_ColorTRANSPARENT;
search_engines::ChoiceData search_engine_choice_data;
};
// Cancels any current signin interception and resets the interceptor to its
// initial state.
void Reset();
// Helper functions to determine which interception UI should be shown.
const ProfileAttributesEntry* ShouldShowProfileSwitchBubble(
const std::string& intercepted_email,
ProfileAttributesStorage* profile_attribute_storage) const;
bool ShouldEnforceEnterpriseProfileSeparation(
const AccountInfo& intercepted_account_info) const;
bool ShouldShowEnterpriseDialog(
const AccountInfo& intercepted_account_info) const;
bool ShouldShowEnterpriseBubble(
const AccountInfo& intercepted_account_info) const;
bool ShouldShowMultiUserBubble(
const AccountInfo& intercepted_account_info) const;
bool ShouldShowChromeSigninBubble(const GaiaId& gaia_id);
// Helper function to call `delegate_->ShowSigninInterceptionBubble()`.
void ShowSigninInterceptionBubble(
const WebSigninInterceptor::Delegate::BubbleParameters& bubble_parameters,
base::OnceCallback<void(SigninInterceptionResult)> callback);
// Ensure that we are observing changes in extended account info. Idempotent.
void EnsureObservingExtendedAccountInfo();
// Can be called at any time, and will either process the interception or
// register the required observers and wait for async operations to complete.
void ProcessInterceptionOrWait(const AccountInfo& info, bool timed_out);
void OnInterceptionReadyToBeProcessed(const AccountInfo& info);
// signin::IdentityManager::Observer:
void OnExtendedAccountInfoUpdated(const AccountInfo& info) override;
void OnExtendedAccountInfoRemoved(const AccountInfo& info) override;
// Called when one or more of the async info fetches times out.
void OnInterceptionInfoFetchTimeout();
// Called after the user chose whether a new profile would be created.
void OnProfileCreationChoice(const AccountInfo& account_info,
SkColor profile_color,
SigninInterceptionResult create);
// Called after the user chose whether the session should continue in a new
// profile.
void OnProfileSwitchChoice(const std::string& email,
const base::FilePath& profile_path,
SigninInterceptionResult switch_profile);
// Called after the user chose whether they want to sign in to chrome or not
// via the Chrome Signin Bubble.
void OnChromeSigninChoice(const AccountInfo& account_info,
SigninInterceptionResult result);
// Processes the intercept result:
// - counting dismissals, after 5 dismissals the result is transformed to a
// decline.
// - saving the accept/decline result in a pref.
// - returns the processed result.
SigninInterceptionResult ProcessChromeSigninUserChoice(
SigninInterceptionResult result,
const GaiaId& gaia_id);
// A non `std::nullopt` `profile_presets` will be applied to the
// `new_profile` when the function is called.
void OnNewSignedInProfileCreated(
std::optional<ProfilePresets> profile_presets,
Profile* new_profile);
// Called after the user choses whether the session should continue in a new
// work profile or not. If the user choses not to continue in a work profile,
// the account is signed out.
void OnEnterpriseProfileCreationResult(const AccountInfo& account_info,
SkColor profile_color,
SigninInterceptionResult create);
// Called when the new browser is created after interception. Passed as
// callback to `session_startup_helper_`.
void OnNewBrowserCreated(bool is_new_profile);
// Returns a 8-bit hash of the email that can be persisted.
static std::string GetPersistentEmailHash(const std::string& email);
// Increments the current entry count corresponding to the `email` of the
// given pref. The given `pref_name` is expected to be a DictionaryPref with a
// key as a hash string computed from an email string. These prefs are used to
// remember the user choices/number of times the bubble is shown to them per
// account/email.
// Only a hash of the email is saved, as Chrome does not need to store the
// actual email, but only need to compare emails. The hash has low entropy to
// ensure it cannot be reversed.
// Returns the incremented value of the pref.
size_t IncrementEmailToCountDictionaryPref(const char* pref_name,
const std::string& email);
// Records the number of times the user previously dismissed the Chrome Signin
// bubble when accepting/declining it. Result is expected to be either
// `SigninInterceptionResult::kAccepted` or
// `SigninInterceptionResult::kDeclined`.
void RecordChromeSigninNumberOfDismissesForAccount(
const GaiaId& gaia_id,
SigninInterceptionResult result);
// Checks if the user previously declined 2 times creating a new profile for
// this account.
bool HasUserDeclinedProfileCreation(const std::string& email) const;
// Fetches the value of the cloud user level value of the
// ManagedAccountsSigninRestriction policy for 'account_info' and runs
// `callback` with the result. This is a network call that has a 5 seconds
// timeout.
void EnsureAccountLevelSigninRestrictionFetchInProgress(
const AccountInfo& account_info,
base::OnceCallback<void(const policy::ProfileSeparationPolicies&)>
callback);
// Called when the the value of the cloud user level value of the
// ManagedAccountsSigninRestriction is received.
void OnAccountLevelManagedAccountsSigninRestrictionReceived(
const AccountInfo& account_info,
const policy::ProfileSeparationPolicies& profile_separation_policies);
// Records the heuristic outcome and latency metrics.
void RecordSigninInterceptionHeuristicOutcome(
SigninInterceptionHeuristicOutcome outcome) const;
// Returns true if we have all the extended account information which might
// factor in to the intercept heuristic. If we don't have 'Full' information,
// but do have the 'Required' information above, we will make a best-effort
// decision based on sensible defaults.
// Returns false otherwise.
bool IsFullExtendedAccountInfoAvailable(
const AccountInfo& account_info) const;
// Struct to ease the resetting of the `DiceWebSigninInterceptor` class
// through the `DiceWebSigninInterceptor::Reset()` method.
// It should hold the data that are variable between different intereceptions.
struct ResetableState {
ResetableState();
~ResetableState();
// Used in the profile that was created after the interception succeeded.
std::unique_ptr<DiceInterceptedSessionStartupHelper>
session_startup_helper_;
// Members below are related to the interception in progress.
base::WeakPtr<content::WebContents> web_contents_;
bool is_interception_in_progress_ = false;
CoreAccountId account_id_;
bool new_account_interception_ = false;
bool intercepted_account_management_accepted_ = false;
std::optional<WebSigninInterceptor::SigninInterceptionType>
interception_type_;
signin_metrics::AccessPoint access_point_ =
signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
std::optional<ShouldShowChromeSigninBubbleWithReason>
should_show_chrome_signin_bubble_;
// Timeout for waiting for full information to be available (see
// `ProcessInterceptionOrWait()`).
base::CancelableOnceCallback<void()> interception_info_available_timeout_;
std::unique_ptr<DiceSignedInProfileCreator> dice_signed_in_profile_creator_;
// Used to retain the interception UI bubble until profile creation
// completes.
std::unique_ptr<ScopedWebSigninInterceptionBubbleHandle>
interception_bubble_handle_;
// Used for metrics.
base::TimeTicks interception_start_time_;
bool was_interception_ui_displayed_ = false;
// Used to fetch the cloud user level policy value of the profile separation
// policies. This can only fetch one policy value for one account at the
// time.
std::unique_ptr<policy::UserCloudSigninRestrictionPolicyFetcher>
account_level_signin_restriction_policy_fetcher_;
// Value of the profile separation policies for the intercepted account. If
// no value is set, then we have not yet received the policy value.
std::optional<policy::ProfileSeparationPolicies>
intercepted_account_profile_separation_policies_;
};
const raw_ptr<Profile> profile_;
const raw_ptr<signin::IdentityManager> identity_manager_;
std::unique_ptr<WebSigninInterceptor::Delegate> delegate_;
base::ScopedObservation<signin::IdentityManager,
signin::IdentityManager::Observer>
account_info_update_observation_{this};
std::unique_ptr<ResetableState> state_;
// Value that should be return when trying to the value of the profile
// separation policies for the intercepted account. This should never be
// used in place of `intercepted_account_profile_separation_policies_`.
// This field is excluded from `ResetableState` as tests do not expect to
// reset this value, it is expected to be sticky across tests.
std::optional<policy::ProfileSeparationPolicies>
intercepted_account_profile_separation_policies_response_for_testing_;
};
#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_