1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/signin/dice_web_signin_interceptor.h"
6
7 #include <string>
8
9 #include "base/check.h"
10 #include "base/hash/hash.h"
11 #include "base/i18n/case_conversion.h"
12 #include "base/metrics/histogram_functions.h"
13 #include "base/optional.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/thread_task_runner_handle.h"
16 #include "base/time/time.h"
17 #include "chrome/browser/browser_process.h"
18 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
19 #include "chrome/browser/profiles/profile_attributes_entry.h"
20 #include "chrome/browser/profiles/profile_attributes_storage.h"
21 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
22 #include "chrome/browser/profiles/profile_manager.h"
23 #include "chrome/browser/profiles/profile_metrics.h"
24 #include "chrome/browser/profiles/profiles_state.h"
25 #include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
26 #include "chrome/browser/signin/dice_signed_in_profile_creator.h"
27 #include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
28 #include "chrome/browser/signin/identity_manager_factory.h"
29 #include "chrome/browser/signin/signin_features.h"
30 #include "chrome/browser/themes/theme_service.h"
31 #include "chrome/browser/themes/theme_service_factory.h"
32 #include "chrome/browser/ui/browser_finder.h"
33 #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
34 #include "chrome/browser/ui/signin/profile_colors_util.h"
35 #include "chrome/common/pref_names.h"
36 #include "chrome/common/search/generated_colors_info.h"
37 #include "chrome/common/themes/autogenerated_theme_util.h"
38 #include "components/password_manager/core/browser/password_manager.h"
39 #include "components/password_manager/core/common/password_manager_ui.h"
40 #include "components/pref_registry/pref_registry_syncable.h"
41 #include "components/prefs/pref_service.h"
42 #include "components/prefs/scoped_user_pref_update.h"
43 #include "components/signin/public/identity_manager/identity_manager.h"
44 #include "google_apis/gaia/gaia_auth_util.h"
45 #include "ui/base/l10n/l10n_util.h"
46
47 namespace {
48
49 constexpr char kProfileCreationInterceptionDeclinedPref[] =
50 "signin.ProfileCreationInterceptionDeclinedPref";
51
RecordSigninInterceptionHeuristicOutcome(SigninInterceptionHeuristicOutcome outcome)52 void RecordSigninInterceptionHeuristicOutcome(
53 SigninInterceptionHeuristicOutcome outcome) {
54 base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
55 }
56
IsProfileCreationAllowed()57 bool IsProfileCreationAllowed() {
58 PrefService* service = g_browser_process->local_state();
59 DCHECK(service);
60 return service->GetBoolean(prefs::kBrowserAddPersonEnabled);
61 }
62
63 // Helper function to return the primary account info. The returned info is
64 // empty if there is no primary account, and non-empty otherwise. Extended
65 // fields may be missing if they are not available.
GetPrimaryAccountInfo(signin::IdentityManager * manager)66 AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) {
67 CoreAccountInfo primary_core_account_info =
68 manager->GetPrimaryAccountInfo(signin::ConsentLevel::kNotRequired);
69 if (primary_core_account_info.IsEmpty())
70 return AccountInfo();
71
72 base::Optional<AccountInfo> primary_account_info =
73 manager->FindExtendedAccountInfoForAccountWithRefreshToken(
74 primary_core_account_info);
75
76 if (primary_account_info)
77 return *primary_account_info;
78
79 // Return an AccountInfo without extended fields, based on the core info.
80 AccountInfo account_info;
81 account_info.gaia = primary_core_account_info.gaia;
82 account_info.email = primary_core_account_info.email;
83 account_info.account_id = primary_core_account_info.account_id;
84 return account_info;
85 }
86
HasNoBrowser(content::WebContents * web_contents)87 bool HasNoBrowser(content::WebContents* web_contents) {
88 return chrome::FindBrowserWithWebContents(web_contents) == nullptr;
89 }
90
91 } // namespace
92
SigninInterceptionHeuristicOutcomeIsSuccess(SigninInterceptionHeuristicOutcome outcome)93 bool SigninInterceptionHeuristicOutcomeIsSuccess(
94 SigninInterceptionHeuristicOutcome outcome) {
95 return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise ||
96 outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser ||
97 outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
98 }
99
DiceWebSigninInterceptor(Profile * profile,std::unique_ptr<Delegate> delegate)100 DiceWebSigninInterceptor::DiceWebSigninInterceptor(
101 Profile* profile,
102 std::unique_ptr<Delegate> delegate)
103 : profile_(profile),
104 identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
105 delegate_(std::move(delegate)) {
106 DCHECK(profile_);
107 DCHECK(identity_manager_);
108 DCHECK(delegate_);
109 }
110
111 DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default;
112
113 // static
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)114 void DiceWebSigninInterceptor::RegisterProfilePrefs(
115 user_prefs::PrefRegistrySyncable* registry) {
116 registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref);
117 registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true);
118 }
119
120 base::Optional<SigninInterceptionHeuristicOutcome>
GetHeuristicOutcome(bool is_new_account,bool is_sync_signin,const std::string & email,const ProfileAttributesEntry ** entry) const121 DiceWebSigninInterceptor::GetHeuristicOutcome(
122 bool is_new_account,
123 bool is_sync_signin,
124 const std::string& email,
125 const ProfileAttributesEntry** entry) const {
126 if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled))
127 return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled;
128
129 if (is_sync_signin) {
130 // Do not intercept signins from the Sync startup flow.
131 // Note: |is_sync_signin| is an approximation, and in rare cases it may be
132 // true when in fact the signin was not a sync signin. In this case the
133 // interception is missed.
134 return SigninInterceptionHeuristicOutcome::kAbortSyncSignin;
135 }
136 if (!is_new_account) {
137 // Do not intercept reauth.
138 return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew;
139 }
140
141 const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
142 email,
143 &g_browser_process->profile_manager()->GetProfileAttributesStorage());
144 if (switch_to_entry) {
145 if (entry)
146 *entry = switch_to_entry;
147 return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
148 }
149
150 // From this point the remaining possible interceptions involve creating a new
151 // profile.
152 if (!IsProfileCreationAllowed()) {
153 return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed;
154 }
155
156 std::vector<CoreAccountInfo> accounts_in_chrome =
157 identity_manager_->GetAccountsWithRefreshTokens();
158 if (accounts_in_chrome.size() == 0 ||
159 (accounts_in_chrome.size() == 1 &&
160 gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) {
161 // Enterprise and multi-user bubbles are only shown if there are multiple
162 // accounts. The intercepted account may not be added to chrome yet.
163 return SigninInterceptionHeuristicOutcome::kAbortSingleAccount;
164 }
165
166 if (HasUserDeclinedProfileCreation(email)) {
167 return SigninInterceptionHeuristicOutcome::
168 kAbortUserDeclinedProfileForAccount;
169 }
170
171 return base::nullopt;
172 }
173
MaybeInterceptWebSignin(content::WebContents * web_contents,CoreAccountId account_id,bool is_new_account,bool is_sync_signin)174 void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
175 content::WebContents* web_contents,
176 CoreAccountId account_id,
177 bool is_new_account,
178 bool is_sync_signin) {
179 if (!base::FeatureList::IsEnabled(kDiceWebSigninInterceptionFeature))
180 return;
181
182 if (is_interception_in_progress_) {
183 // Multiple concurrent interceptions are not supported.
184 RecordSigninInterceptionHeuristicOutcome(
185 SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
186 return;
187 }
188
189 if (HasNoBrowser(web_contents)) {
190 // Do not intercept from the profile creation flow.
191 RecordSigninInterceptionHeuristicOutcome(
192 SigninInterceptionHeuristicOutcome::kAbortNoBrowser);
193 return;
194 }
195
196 // Do not show the interception UI if a password update is required: both
197 // bubbles cannot be shown at the same time and the password update is more
198 // important.
199 ChromePasswordManagerClient* password_manager_client =
200 ChromePasswordManagerClient::FromWebContents(web_contents);
201 if (password_manager_client && password_manager_client->GetPasswordManager()
202 ->IsFormManagerPendingPasswordUpdate()) {
203 RecordSigninInterceptionHeuristicOutcome(
204 SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending);
205 return;
206 }
207
208 ManagePasswordsUIController* password_controller =
209 ManagePasswordsUIController::FromWebContents(web_contents);
210 if (password_controller &&
211 password_controller->GetState() ==
212 password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) {
213 RecordSigninInterceptionHeuristicOutcome(
214 SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate);
215 return;
216 }
217
218 base::Optional<AccountInfo> account_info =
219 identity_manager_
220 ->FindExtendedAccountInfoForAccountWithRefreshTokenByAccountId(
221 account_id);
222 DCHECK(account_info) << "Intercepting unknown account.";
223 const ProfileAttributesEntry* entry = nullptr;
224 base::Optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
225 GetHeuristicOutcome(is_new_account, is_sync_signin, account_info->email,
226 &entry);
227 account_id_ = account_id;
228 is_interception_in_progress_ = true;
229 Observe(web_contents);
230
231 if (heuristic_outcome) {
232 RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome);
233 if (*heuristic_outcome ==
234 SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) {
235 DCHECK(entry);
236 Delegate::BubbleParameters bubble_parameters{
237 SigninInterceptionType::kProfileSwitch, *account_info,
238 GetPrimaryAccountInfo(identity_manager_),
239 entry->GetProfileThemeColors().profile_highlight_color};
240 delegate_->ShowSigninInterceptionBubble(
241 web_contents, bubble_parameters,
242 base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice,
243 base::Unretained(this), entry->GetPath()));
244 was_interception_ui_displayed_ = true;
245 } else {
246 // Interception is aborted.
247 DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome));
248 Reset();
249 }
250 return;
251 }
252
253 account_info_fetch_start_time_ = base::TimeTicks::Now();
254 if (account_info->IsValid()) {
255 OnExtendedAccountInfoUpdated(*account_info);
256 } else {
257 on_account_info_update_timeout_.Reset(base::BindOnce(
258 &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout,
259 base::Unretained(this)));
260 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
261 FROM_HERE, on_account_info_update_timeout_.callback(),
262 base::TimeDelta::FromSeconds(5));
263 account_info_update_observer_.Add(identity_manager_);
264 }
265 }
266
CreateBrowserAfterSigninInterception(CoreAccountId account_id,content::WebContents * intercepted_contents,bool show_customization_bubble)267 void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception(
268 CoreAccountId account_id,
269 content::WebContents* intercepted_contents,
270 bool show_customization_bubble) {
271 DCHECK(!session_startup_helper_);
272 session_startup_helper_ =
273 std::make_unique<DiceInterceptedSessionStartupHelper>(
274 profile_, account_id, intercepted_contents);
275 session_startup_helper_->Startup(
276 base::Bind(&DiceWebSigninInterceptor::OnNewBrowserCreated,
277 base::Unretained(this), show_customization_bubble));
278 }
279
Shutdown()280 void DiceWebSigninInterceptor::Shutdown() {
281 if (is_interception_in_progress_ && !was_interception_ui_displayed_) {
282 RecordSigninInterceptionHeuristicOutcome(
283 SigninInterceptionHeuristicOutcome::kAbortShutdown);
284 }
285 Reset();
286 }
287
Reset()288 void DiceWebSigninInterceptor::Reset() {
289 Observe(/*web_contents=*/nullptr);
290 account_info_update_observer_.RemoveAll();
291 on_account_info_update_timeout_.Cancel();
292 is_interception_in_progress_ = false;
293 account_id_ = CoreAccountId();
294 dice_signed_in_profile_creator_.reset();
295 was_interception_ui_displayed_ = false;
296 account_info_fetch_start_time_ = base::TimeTicks();
297 profile_creation_start_time_ = base::TimeTicks();
298 }
299
300 const ProfileAttributesEntry*
ShouldShowProfileSwitchBubble(const std::string & intercepted_email,ProfileAttributesStorage * profile_attribute_storage) const301 DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble(
302 const std::string& intercepted_email,
303 ProfileAttributesStorage* profile_attribute_storage) const {
304 // Check if there is already an existing profile with this account.
305 base::FilePath profile_path = profile_->GetPath();
306 for (const auto* entry :
307 profile_attribute_storage->GetAllProfilesAttributes()) {
308 if (entry->GetPath() == profile_path)
309 continue;
310 if (gaia::AreEmailsSame(intercepted_email,
311 base::UTF16ToUTF8(entry->GetUserName()))) {
312 return entry;
313 }
314 }
315 return nullptr;
316 }
317
ShouldShowEnterpriseBubble(const AccountInfo & intercepted_account_info) const318 bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble(
319 const AccountInfo& intercepted_account_info) const {
320 DCHECK(intercepted_account_info.IsValid());
321 // Check if the intercepted account or the primary account is managed.
322 CoreAccountInfo primary_core_account_info =
323 identity_manager_->GetPrimaryAccountInfo(
324 signin::ConsentLevel::kNotRequired);
325
326 if (primary_core_account_info.IsEmpty() ||
327 primary_core_account_info.account_id ==
328 intercepted_account_info.account_id) {
329 return false;
330 }
331
332 if (intercepted_account_info.hosted_domain != kNoHostedDomainFound)
333 return true;
334
335 base::Optional<AccountInfo> primary_account_info =
336 identity_manager_->FindExtendedAccountInfoForAccountWithRefreshToken(
337 primary_core_account_info);
338 if (!primary_account_info || !primary_account_info->IsValid())
339 return false;
340
341 return primary_account_info->hosted_domain != kNoHostedDomainFound;
342 }
343
ShouldShowMultiUserBubble(const AccountInfo & intercepted_account_info) const344 bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble(
345 const AccountInfo& intercepted_account_info) const {
346 DCHECK(intercepted_account_info.IsValid());
347 if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u)
348 return false;
349 // Check if the account has the same name as another account in the profile.
350 for (const auto& account_info :
351 identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) {
352 if (account_info.account_id == intercepted_account_info.account_id)
353 continue;
354 // Case-insensitve comparison supporting non-ASCII characters.
355 if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) ==
356 base::i18n::FoldCase(
357 base::UTF8ToUTF16(intercepted_account_info.given_name))) {
358 return false;
359 }
360 }
361 return true;
362 }
363
OnExtendedAccountInfoUpdated(const AccountInfo & info)364 void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated(
365 const AccountInfo& info) {
366 if (info.account_id != account_id_)
367 return;
368 if (!info.IsValid())
369 return;
370
371 account_info_update_observer_.RemoveAll();
372 on_account_info_update_timeout_.Cancel();
373 base::UmaHistogramTimes(
374 "Signin.Intercept.AccountInfoFetchDuration",
375 base::TimeTicks::Now() - account_info_fetch_start_time_);
376
377 base::Optional<SigninInterceptionType> interception_type;
378
379 if (ShouldShowEnterpriseBubble(info))
380 interception_type = SigninInterceptionType::kEnterprise;
381 else if (ShouldShowMultiUserBubble(info))
382 interception_type = SigninInterceptionType::kMultiUser;
383
384 if (!interception_type) {
385 // Signin should not be intercepted.
386 RecordSigninInterceptionHeuristicOutcome(
387 SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
388 Reset();
389 return;
390 }
391
392 ProfileAttributesEntry* entry;
393 g_browser_process->profile_manager()
394 ->GetProfileAttributesStorage()
395 .GetProfileAttributesWithPath(profile_->GetPath(), &entry);
396 SkColor profile_color = GenerateNewProfileColor(entry).color;
397 Delegate::BubbleParameters bubble_parameters{
398 *interception_type, info, GetPrimaryAccountInfo(identity_manager_),
399 GetAutogeneratedThemeColors(profile_color).frame_color};
400 delegate_->ShowSigninInterceptionBubble(
401 web_contents(), bubble_parameters,
402 base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
403 base::Unretained(this), info, profile_color));
404 was_interception_ui_displayed_ = true;
405 RecordSigninInterceptionHeuristicOutcome(
406 *interception_type == SigninInterceptionType::kEnterprise
407 ? SigninInterceptionHeuristicOutcome::kInterceptEnterprise
408 : SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
409 }
410
OnExtendedAccountInfoFetchTimeout()411 void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() {
412 RecordSigninInterceptionHeuristicOutcome(
413 SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout);
414 Reset();
415 }
416
OnProfileCreationChoice(const AccountInfo & account_info,SkColor profile_color,SigninInterceptionResult create)417 void DiceWebSigninInterceptor::OnProfileCreationChoice(
418 const AccountInfo& account_info,
419 SkColor profile_color,
420 SigninInterceptionResult create) {
421 if (create != SigninInterceptionResult::kAccepted) {
422 if (create == SigninInterceptionResult::kDeclined)
423 RecordProfileCreationDeclined(account_info.email);
424 Reset();
425 return;
426 }
427
428 profile_creation_start_time_ = base::TimeTicks::Now();
429 base::string16 profile_name;
430 profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info);
431
432 DCHECK(!dice_signed_in_profile_creator_);
433 // Unretained is fine because the profile creator is owned by this.
434 dice_signed_in_profile_creator_ =
435 std::make_unique<DiceSignedInProfileCreator>(
436 profile_, account_id_, profile_name,
437 profiles::GetPlaceholderAvatarIndex(),
438 base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
439 base::Unretained(this), profile_color));
440 }
441
OnProfileSwitchChoice(const base::FilePath & profile_path,SigninInterceptionResult switch_profile)442 void DiceWebSigninInterceptor::OnProfileSwitchChoice(
443 const base::FilePath& profile_path,
444 SigninInterceptionResult switch_profile) {
445 if (switch_profile != SigninInterceptionResult::kAccepted) {
446 Reset();
447 return;
448 }
449
450 profile_creation_start_time_ = base::TimeTicks::Now();
451 DCHECK(!dice_signed_in_profile_creator_);
452 // Unretained is fine because the profile creator is owned by this.
453 dice_signed_in_profile_creator_ =
454 std::make_unique<DiceSignedInProfileCreator>(
455 profile_, account_id_, profile_path,
456 base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
457 base::Unretained(this), base::nullopt));
458 }
459
OnNewSignedInProfileCreated(base::Optional<SkColor> profile_color,Profile * new_profile)460 void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
461 base::Optional<SkColor> profile_color,
462 Profile* new_profile) {
463 DCHECK(dice_signed_in_profile_creator_);
464 dice_signed_in_profile_creator_.reset();
465
466 if (!new_profile) {
467 Reset();
468 return;
469 }
470
471 bool show_customization_bubble = false;
472 if (profile_color.has_value()) {
473 // The profile color is defined only when the profile has just been created
474 // (with interception type kMultiUser or kEnterprise). If the profile is not
475 // new (kProfileSwitch), then the color is not updated.
476 base::UmaHistogramTimes(
477 "Signin.Intercept.ProfileCreationDuration",
478 base::TimeTicks::Now() - profile_creation_start_time_);
479 ProfileMetrics::LogProfileAddNewUser(
480 ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION);
481 // Apply the new color to the profile.
482 ThemeServiceFactory::GetForProfile(new_profile)
483 ->BuildAutogeneratedThemeFromColor(*profile_color);
484 // Show the customization UI to allow changing the color.
485 show_customization_bubble = true;
486 } else {
487 base::UmaHistogramTimes(
488 "Signin.Intercept.ProfileSwitchDuration",
489 base::TimeTicks::Now() - profile_creation_start_time_);
490 }
491
492 // Work is done in this profile, the flow continues in the
493 // DiceWebSigninInterceptor that is attached to the new profile.
494 DiceWebSigninInterceptorFactory::GetForProfile(new_profile)
495 ->CreateBrowserAfterSigninInterception(account_id_, web_contents(),
496 show_customization_bubble);
497 Reset();
498 }
499
OnNewBrowserCreated(bool show_customization_bubble)500 void DiceWebSigninInterceptor::OnNewBrowserCreated(
501 bool show_customization_bubble) {
502 session_startup_helper_.reset();
503 if (show_customization_bubble) {
504 Browser* browser = chrome::FindBrowserWithProfile(profile_);
505 DCHECK(browser);
506 delegate_->ShowProfileCustomizationBubble(browser);
507 }
508 }
509
510 // static
GetPersistentEmailHash(const std::string & email)511 std::string DiceWebSigninInterceptor::GetPersistentEmailHash(
512 const std::string& email) {
513 int hash = base::PersistentHash(
514 gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) &
515 0xFF;
516 return base::StringPrintf("email_%i", hash);
517 }
518
RecordProfileCreationDeclined(const std::string & email)519 void DiceWebSigninInterceptor::RecordProfileCreationDeclined(
520 const std::string& email) {
521 DictionaryPrefUpdate update(profile_->GetPrefs(),
522 kProfileCreationInterceptionDeclinedPref);
523 std::string key = GetPersistentEmailHash(email);
524 base::Optional<int> declined_count = update->FindIntKey(key);
525 update->SetIntKey(
526 key, declined_count.has_value() ? declined_count.value() + 1 : 1);
527 }
528
HasUserDeclinedProfileCreation(const std::string & email) const529 bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
530 const std::string& email) const {
531 const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary(
532 kProfileCreationInterceptionDeclinedPref);
533 base::Optional<int> declined_count =
534 pref_data->FindIntKey(GetPersistentEmailHash(email));
535 // Check if the user declined 3 times.
536 constexpr int kMaxProfileCreationDeclinedCount = 3;
537 return declined_count &&
538 declined_count.value() >= kMaxProfileCreationDeclinedCount;
539 }
540