summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/signin/dice_web_signin_interceptor.h')
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor.h411
1 files changed, 411 insertions, 0 deletions
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
new file mode 100644
index 00000000000..0e3a03f41c3
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
@@ -0,0 +1,411 @@
+// 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.
+
+#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/cancelable_callback.h"
+#include "base/feature_list.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/ui/webui/signin/enterprise_profile_welcome_ui.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.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 Browser;
+class DiceSignedInProfileCreator;
+class DiceInterceptedSessionStartupHelper;
+class Profile;
+class ProfileAttributesEntry;
+class ProfileAttributesStorage;
+
+// Outcome of the interception heuristic (decision whether the interception
+// bubble is shown or not).
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionHeuristicOutcome {
+ // Interception succeeded:
+ kInterceptProfileSwitch = 0,
+ kInterceptMultiUser = 1,
+ kInterceptEnterprise = 2,
+
+ // Interception aborted:
+ // This is a "Sync" sign in and not a "web" sign in.
+ kAbortSyncSignin = 3,
+ // Another interception is already in progress.
+ kAbortInterceptInProgress = 4,
+ // This is not a new account (reauth).
+ kAbortAccountNotNew = 5,
+ // New profile is not offered when there is only one account.
+ kAbortSingleAccount = 6,
+ // Extended account info could not be downloaded.
+ kAbortAccountInfoTimeout = 7,
+ // Account info not compatible with interception (e.g. same Gaia name).
+ kAbortAccountInfoNotCompatible = 8,
+ // Profile creation disallowed.
+ kAbortProfileCreationDisallowed = 9,
+ // The interceptor was shut down before the heuristic completed.
+ kAbortShutdown = 10,
+ // The interceptor is not offered when WebContents has no browser associated.
+ kAbortNoBrowser = 11,
+ // A password update is required for the account, and this takes priority over
+ // signin interception.
+ kAbortPasswordUpdate = 12,
+ // A password update will be required for the account: the password used on
+ // the form does not match the stored password.
+ kAbortPasswordUpdatePending = 13,
+ // The user already declined a new profile for this account, the UI is not
+ // shown again.
+ kAbortUserDeclinedProfileForAccount = 14,
+ // Signin interception is disabled by the SigninInterceptionEnabled policy.
+ kAbortInterceptionDisabled = 15,
+
+ // Interception succeeded when enteprise account separation is mandatory.
+ kInterceptEnterpriseForced = 16,
+ kInterceptEnterpriseForcedProfileSwitch = 17,
+
+ // The interceptor is not triggered if the tab has already been closed.
+ kAbortTabClosed = 18,
+
+ kMaxValue = kAbortTabClosed,
+};
+
+// User selection in the interception bubble.
+enum class SigninInterceptionUserChoice { kAccept, kDecline, kGuest };
+
+// User action resulting from the interception bubble.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionResult {
+ kAccepted = 0,
+ kDeclined = 1,
+ kIgnored = 2,
+
+ // Used when the bubble was not shown because it's not implemented.
+ kNotDisplayed = 3,
+
+ // Accepted to be opened in Guest profile.
+ kAcceptedWithGuest = 4,
+
+ kMaxValue = kAcceptedWithGuest,
+};
+
+// The ScopedDiceWebSigninInterceptionBubbleHandle closes the signin intercept
+// bubble when it is destroyed, if the bubble is still opened. Note that this
+// handle does not prevent the bubble from being closed for other reasons.
+class ScopedDiceWebSigninInterceptionBubbleHandle {
+ public:
+ virtual ~ScopedDiceWebSigninInterceptionBubbleHandle() = 0;
+};
+
+// Returns whether the heuristic outcome is a success (the signin should be
+// intercepted).
+bool SigninInterceptionHeuristicOutcomeIsSuccess(
+ SigninInterceptionHeuristicOutcome outcome);
+
+// 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:
+ enum class SigninInterceptionType {
+ kProfileSwitch,
+ kEnterprise,
+ kMultiUser,
+ kEnterpriseForced,
+ kProfileSwitchForced
+ };
+
+ // Delegate class responsible for showing the various interception UIs.
+ class Delegate {
+ public:
+ // Parameters for interception bubble UIs.
+ struct BubbleParameters {
+ SigninInterceptionType interception_type;
+ AccountInfo intercepted_account;
+ AccountInfo primary_account;
+ SkColor profile_highlight_color;
+ bool show_guest_option;
+ };
+
+ virtual ~Delegate() = default;
+
+ // Shows the signin interception bubble and calls |callback| to indicate
+ // whether the user should continue in a new profile.
+ // The callback is never called if the delegate is deleted before it
+ // completes.
+ // May return a nullptr handle if the bubble cannot be shown.
+ // Warning: the handle closes the bubble when it is destroyed ; it is the
+ // responsibility of the caller to keep the handle alive until the bubble
+ // should be closed.
+ // The callback must not be called synchronously if this function returns a
+ // valid handle (because the caller needs to be able to close the bubble
+ // from the callback).
+ virtual std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) = 0;
+
+ // Shows the profile customization bubble.
+ virtual void ShowProfileCustomizationBubble(Browser* browser) = 0;
+ };
+
+ DiceWebSigninInterceptor(Profile* profile,
+ std::unique_ptr<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,
+ 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<ScopedDiceWebSigninInterceptionBubbleHandle>
+ bubble_handle,
+ bool is_new_profile);
+
+ // 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).
+ absl::optional<SigninInterceptionHeuristicOutcome> GetHeuristicOutcome(
+ bool is_new_account,
+ bool is_sync_signin,
+ const std::string& email,
+ 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 is_interception_in_progress_;
+ }
+
+ void SetAccountLevelSigninRestrictionFetchResultForTesting(
+ absl::optional<std::string> value) {
+ intercepted_account_level_policy_value_fetch_result_for_testing_ =
+ std::move(value);
+ }
+
+ // KeyedService:
+ void Shutdown() override;
+
+ private:
+ 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, PersistentHash);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparation);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationWithoutUPA);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimary);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestAccountLevelPolicy);
+ FRIEND_TEST_ALL_PREFIXES(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestNoForcedInterception);
+
+ // 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 ShouldShowEnterpriseBubble(
+ const AccountInfo& intercepted_account_info) const;
+ bool ShouldShowMultiUserBubble(
+ const AccountInfo& intercepted_account_info) const;
+
+ void OnInterceptionReadyToBeProcessed(const AccountInfo& info);
+
+ // signin::IdentityManager::Observer:
+ void OnExtendedAccountInfoUpdated(const AccountInfo& info) override;
+
+ // Called when the extended account info was not updated after a timeout.
+ void OnExtendedAccountInfoFetchTimeout();
+
+ // 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 when the new profile is created or loaded from disk.
+ // `profile_color` is set as theme color for the profile ; it should be
+ // nullopt if the profile is not new (loaded from disk).
+ void OnNewSignedInProfileCreated(absl::optional<SkColor> profile_color,
+ 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);
+
+ // Should be called when the user declines profile creation, in order to
+ // remember their decision. This information is stored in prefs. 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.
+ void RecordProfileCreationDeclined(const std::string& email);
+
+ // 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 FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ const AccountInfo& account_info,
+ base::OnceCallback<void(const std::string&)> callback);
+
+ // Called when the the value of the cloud user level value of the
+ // ManagedAccountsSigninRestriction is received.
+ void OnAccountLevelManagedAccountsSigninRestrictionReceived(
+ bool timed_out,
+ const AccountInfo& account_info,
+ const std::string& signin_restriction);
+
+ const raw_ptr<Profile> profile_;
+ const raw_ptr<signin::IdentityManager> identity_manager_;
+ std::unique_ptr<Delegate> delegate_;
+
+ // 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;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ account_info_update_observation_{this};
+ // Timeout for the fetch of the extended account info. The signin interception
+ // is cancelled if the account info cannot be fetched quickly.
+ base::CancelableOnceCallback<void()> on_account_info_update_timeout_;
+ std::unique_ptr<DiceSignedInProfileCreator> dice_signed_in_profile_creator_;
+ // Used to retain the interception UI bubble until profile creation completes.
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ interception_bubble_handle_;
+ // Used for metrics:
+ bool was_interception_ui_displayed_ = false;
+ base::TimeTicks account_info_fetch_start_time_;
+ base::TimeTicks profile_creation_start_time_;
+
+ // Timeout for the fetch of cloud user level policy value of
+ // ManagedAccountsSigninRestriction. The signin interception continue with an
+ // empty value for the policy if we cannot get the value.
+ base::CancelableOnceCallback<void()>
+ on_intercepted_account_level_policy_value_timeout_;
+
+ // Used to fetch the cloud user level policy value of
+ // ManagedAccountsSigninRestriction. 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 ManagedAccountsSigninRestriction for the intercepted account.
+ // If no value is set, then we have not yet received the policy value.
+ absl::optional<std::string> intercepted_account_level_policy_value_;
+ absl::optional<std::string>
+ intercepted_account_level_policy_value_fetch_result_for_testing_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_