account_reconcilor.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1// Copyright 2014 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 "components/signin/core/browser/account_reconcilor.h" 6 7#include <algorithm> 8 9#include "base/bind.h" 10#include "base/json/json_reader.h" 11#include "base/logging.h" 12#include "base/message_loop/message_loop.h" 13#include "base/message_loop/message_loop_proxy.h" 14#include "base/strings/string_number_conversions.h" 15#include "base/time/time.h" 16#include "components/signin/core/browser/profile_oauth2_token_service.h" 17#include "components/signin/core/browser/signin_client.h" 18#include "components/signin/core/browser/signin_metrics.h" 19#include "components/signin/core/common/profile_management_switches.h" 20#include "google_apis/gaia/gaia_auth_fetcher.h" 21#include "google_apis/gaia/gaia_auth_util.h" 22#include "google_apis/gaia/gaia_constants.h" 23#include "google_apis/gaia/gaia_oauth_client.h" 24#include "google_apis/gaia/gaia_urls.h" 25#include "net/cookies/canonical_cookie.h" 26 27 28namespace { 29 30class EmailEqualToFunc : public std::equal_to<std::pair<std::string, bool> > { 31 public: 32 bool operator()(const std::pair<std::string, bool>& p1, 33 const std::pair<std::string, bool>& p2) const; 34}; 35 36bool EmailEqualToFunc::operator()( 37 const std::pair<std::string, bool>& p1, 38 const std::pair<std::string, bool>& p2) const { 39 return p1.second == p2.second && gaia::AreEmailsSame(p1.first, p2.first); 40} 41 42class AreEmailsSameFunc : public std::equal_to<std::string> { 43 public: 44 bool operator()(const std::string& p1, 45 const std::string& p2) const; 46}; 47 48bool AreEmailsSameFunc::operator()( 49 const std::string& p1, 50 const std::string& p2) const { 51 return gaia::AreEmailsSame(p1, p2); 52} 53 54} // namespace 55 56 57AccountReconcilor::AccountReconcilor(ProfileOAuth2TokenService* token_service, 58 SigninManagerBase* signin_manager, 59 SigninClient* client) 60 : token_service_(token_service), 61 signin_manager_(signin_manager), 62 client_(client), 63 merge_session_helper_(token_service_, 64 client->GetURLRequestContext(), 65 this), 66 registered_with_token_service_(false), 67 is_reconcile_started_(false), 68 first_execution_(true), 69 are_gaia_accounts_set_(false) { 70 VLOG(1) << "AccountReconcilor::AccountReconcilor"; 71} 72 73AccountReconcilor::~AccountReconcilor() { 74 VLOG(1) << "AccountReconcilor::~AccountReconcilor"; 75 // Make sure shutdown was called first. 76 DCHECK(!registered_with_token_service_); 77} 78 79void AccountReconcilor::Initialize(bool start_reconcile_if_tokens_available) { 80 VLOG(1) << "AccountReconcilor::Initialize"; 81 RegisterWithSigninManager(); 82 83 // If this user is not signed in, the reconcilor should do nothing but 84 // wait for signin. 85 if (IsProfileConnected()) { 86 RegisterForCookieChanges(); 87 RegisterWithTokenService(); 88 89 // Start a reconcile if the tokens are already loaded. 90 if (start_reconcile_if_tokens_available && 91 token_service_->GetAccounts().size() > 0) { 92 StartReconcile(); 93 } 94 } 95} 96 97void AccountReconcilor::Shutdown() { 98 VLOG(1) << "AccountReconcilor::Shutdown"; 99 merge_session_helper_.CancelAll(); 100 merge_session_helper_.RemoveObserver(this); 101 gaia_fetcher_.reset(); 102 get_gaia_accounts_callbacks_.clear(); 103 UnregisterWithSigninManager(); 104 UnregisterWithTokenService(); 105 UnregisterForCookieChanges(); 106} 107 108void AccountReconcilor::AddMergeSessionObserver( 109 MergeSessionHelper::Observer* observer) { 110 merge_session_helper_.AddObserver(observer); 111} 112 113void AccountReconcilor::RemoveMergeSessionObserver( 114 MergeSessionHelper::Observer* observer) { 115 merge_session_helper_.RemoveObserver(observer); 116} 117 118void AccountReconcilor::RegisterForCookieChanges() { 119 // First clear any existing registration to avoid DCHECKs that can otherwise 120 // go off in some embedders on reauth (e.g., ChromeSigninClient). 121 UnregisterForCookieChanges(); 122 cookie_changed_subscription_ = client_->AddCookieChangedCallback( 123 base::Bind(&AccountReconcilor::OnCookieChanged, base::Unretained(this))); 124} 125 126void AccountReconcilor::UnregisterForCookieChanges() { 127 cookie_changed_subscription_.reset(); 128} 129 130void AccountReconcilor::RegisterWithSigninManager() { 131 signin_manager_->AddObserver(this); 132} 133 134void AccountReconcilor::UnregisterWithSigninManager() { 135 signin_manager_->RemoveObserver(this); 136} 137 138void AccountReconcilor::RegisterWithTokenService() { 139 VLOG(1) << "AccountReconcilor::RegisterWithTokenService"; 140 // During re-auth, the reconcilor will get a callback about successful signin 141 // even when the profile is already connected. Avoid re-registering 142 // with the token service since this will DCHECK. 143 if (registered_with_token_service_) 144 return; 145 146 token_service_->AddObserver(this); 147 registered_with_token_service_ = true; 148} 149 150void AccountReconcilor::UnregisterWithTokenService() { 151 if (!registered_with_token_service_) 152 return; 153 154 token_service_->RemoveObserver(this); 155 registered_with_token_service_ = false; 156} 157 158bool AccountReconcilor::IsProfileConnected() { 159 return signin_manager_->IsAuthenticated(); 160} 161 162void AccountReconcilor::OnCookieChanged(const net::CanonicalCookie* cookie) { 163 if (cookie->Name() == "LSID" && 164 cookie->Domain() == GaiaUrls::GetInstance()->gaia_url().host() && 165 cookie->IsSecure() && cookie->IsHttpOnly()) { 166 VLOG(1) << "AccountReconcilor::OnCookieChanged: LSID changed"; 167 168 // It is possible that O2RT is not available at this moment. 169 if (!token_service_->GetAccounts().size()) { 170 VLOG(1) << "AccountReconcilor::OnCookieChanged: cookie change is ingored" 171 "because O2RT is not available yet."; 172 return; 173 } 174 175 StartReconcile(); 176 } 177} 178 179void AccountReconcilor::OnEndBatchChanges() { 180 VLOG(1) << "AccountReconcilor::OnEndBatchChanges"; 181 StartReconcile(); 182} 183 184void AccountReconcilor::GoogleSigninSucceeded(const std::string& account_id, 185 const std::string& username, 186 const std::string& password) { 187 VLOG(1) << "AccountReconcilor::GoogleSigninSucceeded: signed in"; 188 RegisterForCookieChanges(); 189 RegisterWithTokenService(); 190} 191 192void AccountReconcilor::GoogleSignedOut(const std::string& account_id, 193 const std::string& username) { 194 VLOG(1) << "AccountReconcilor::GoogleSignedOut: signed out"; 195 gaia_fetcher_.reset(); 196 get_gaia_accounts_callbacks_.clear(); 197 AbortReconcile(); 198 UnregisterWithTokenService(); 199 UnregisterForCookieChanges(); 200 PerformLogoutAllAccountsAction(); 201} 202 203void AccountReconcilor::PerformMergeAction(const std::string& account_id) { 204 if (!switches::IsEnableAccountConsistency()) { 205 MarkAccountAsAddedToCookie(account_id); 206 return; 207 } 208 VLOG(1) << "AccountReconcilor::PerformMergeAction: " << account_id; 209 merge_session_helper_.LogIn(account_id); 210} 211 212void AccountReconcilor::PerformLogoutAllAccountsAction() { 213 if (!switches::IsEnableAccountConsistency()) 214 return; 215 VLOG(1) << "AccountReconcilor::PerformLogoutAllAccountsAction"; 216 merge_session_helper_.LogOutAllAccounts(); 217} 218 219void AccountReconcilor::StartReconcile() { 220 if (!IsProfileConnected() || is_reconcile_started_ || 221 get_gaia_accounts_callbacks_.size() > 0 || 222 merge_session_helper_.is_running()) 223 return; 224 225 is_reconcile_started_ = true; 226 227 StartFetchingExternalCcResult(); 228 229 // Reset state for validating gaia cookie. 230 are_gaia_accounts_set_ = false; 231 gaia_accounts_.clear(); 232 GetAccountsFromCookie(base::Bind( 233 &AccountReconcilor::ContinueReconcileActionAfterGetGaiaAccounts, 234 base::Unretained(this))); 235 236 // Reset state for validating oauth2 tokens. 237 primary_account_.clear(); 238 chrome_accounts_.clear(); 239 add_to_cookie_.clear(); 240 ValidateAccountsFromTokenService(); 241} 242 243void AccountReconcilor::GetAccountsFromCookie( 244 GetAccountsFromCookieCallback callback) { 245 get_gaia_accounts_callbacks_.push_back(callback); 246 if (!gaia_fetcher_) { 247 // There is no list account request in flight. 248 gaia_fetcher_.reset(new GaiaAuthFetcher( 249 this, GaiaConstants::kChromeSource, client_->GetURLRequestContext())); 250 gaia_fetcher_->StartListAccounts(); 251 } 252} 253 254void AccountReconcilor::StartFetchingExternalCcResult() { 255 merge_session_helper_.StartFetchingExternalCcResult(); 256} 257 258void AccountReconcilor::OnListAccountsSuccess(const std::string& data) { 259 gaia_fetcher_.reset(); 260 261 // Get account information from response data. 262 std::vector<std::pair<std::string, bool> > gaia_accounts; 263 bool valid_json = gaia::ParseListAccountsData(data, &gaia_accounts); 264 if (!valid_json) { 265 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: parsing error"; 266 } else if (gaia_accounts.size() > 0) { 267 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: " 268 << "Gaia " << gaia_accounts.size() << " accounts, " 269 << "Primary is '" << gaia_accounts[0].first << "'"; 270 } else { 271 VLOG(1) << "AccountReconcilor::OnListAccountsSuccess: No accounts"; 272 } 273 274 // There must be at least one callback waiting for result. 275 DCHECK(!get_gaia_accounts_callbacks_.empty()); 276 277 GoogleServiceAuthError error = 278 !valid_json ? GoogleServiceAuthError( 279 GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE) 280 : GoogleServiceAuthError::AuthErrorNone(); 281 get_gaia_accounts_callbacks_.front().Run(error, gaia_accounts); 282 get_gaia_accounts_callbacks_.pop_front(); 283 284 MayBeDoNextListAccounts(); 285} 286 287void AccountReconcilor::OnListAccountsFailure( 288 const GoogleServiceAuthError& error) { 289 gaia_fetcher_.reset(); 290 VLOG(1) << "AccountReconcilor::OnListAccountsFailure: " << error.ToString(); 291 std::vector<std::pair<std::string, bool> > empty_accounts; 292 293 // There must be at least one callback waiting for result. 294 DCHECK(!get_gaia_accounts_callbacks_.empty()); 295 296 get_gaia_accounts_callbacks_.front().Run(error, empty_accounts); 297 get_gaia_accounts_callbacks_.pop_front(); 298 299 MayBeDoNextListAccounts(); 300} 301 302void AccountReconcilor::MayBeDoNextListAccounts() { 303 if (!get_gaia_accounts_callbacks_.empty()) { 304 gaia_fetcher_.reset(new GaiaAuthFetcher( 305 this, GaiaConstants::kChromeSource, client_->GetURLRequestContext())); 306 gaia_fetcher_->StartListAccounts(); 307 } 308} 309 310void AccountReconcilor::ContinueReconcileActionAfterGetGaiaAccounts( 311 const GoogleServiceAuthError& error, 312 const std::vector<std::pair<std::string, bool> >& accounts) { 313 if (error.state() == GoogleServiceAuthError::NONE) { 314 gaia_accounts_ = accounts; 315 are_gaia_accounts_set_ = true; 316 FinishReconcile(); 317 } else { 318 AbortReconcile(); 319 } 320} 321 322void AccountReconcilor::ValidateAccountsFromTokenService() { 323 primary_account_ = signin_manager_->GetAuthenticatedAccountId(); 324 DCHECK(!primary_account_.empty()); 325 326 chrome_accounts_ = token_service_->GetAccounts(); 327 328 VLOG(1) << "AccountReconcilor::ValidateAccountsFromTokenService: " 329 << "Chrome " << chrome_accounts_.size() << " accounts, " 330 << "Primary is '" << primary_account_ << "'"; 331} 332 333void AccountReconcilor::OnNewProfileManagementFlagChanged( 334 bool new_flag_status) { 335 if (new_flag_status) { 336 // The reconciler may have been newly created just before this call, or may 337 // have already existed and in mid-reconcile. To err on the safe side, force 338 // a restart. 339 Shutdown(); 340 Initialize(true); 341 } else { 342 Shutdown(); 343 } 344} 345 346void AccountReconcilor::FinishReconcile() { 347 VLOG(1) << "AccountReconcilor::FinishReconcile"; 348 DCHECK(are_gaia_accounts_set_); 349 DCHECK(add_to_cookie_.empty()); 350 int number_gaia_accounts = gaia_accounts_.size(); 351 bool are_primaries_equal = number_gaia_accounts > 0 && 352 gaia::AreEmailsSame(primary_account_, gaia_accounts_[0].first); 353 354 // If there are any accounts in the gaia cookie but not in chrome, then 355 // those accounts need to be removed from the cookie. This means we need 356 // to blow the cookie away. 357 int removed_from_cookie = 0; 358 for (size_t i = 0; i < gaia_accounts_.size(); ++i) { 359 const std::string& gaia_account = gaia_accounts_[i].first; 360 if (gaia_accounts_[i].second && 361 chrome_accounts_.end() == 362 std::find_if(chrome_accounts_.begin(), 363 chrome_accounts_.end(), 364 std::bind1st(AreEmailsSameFunc(), gaia_account))) { 365 ++removed_from_cookie; 366 } 367 } 368 369 bool rebuild_cookie = !are_primaries_equal || removed_from_cookie > 0; 370 std::vector<std::pair<std::string, bool> > original_gaia_accounts = 371 gaia_accounts_; 372 if (rebuild_cookie) { 373 VLOG(1) << "AccountReconcilor::FinishReconcile: rebuild cookie"; 374 // Really messed up state. Blow away the gaia cookie completely and 375 // rebuild it, making sure the primary account as specified by the 376 // SigninManager is the first session in the gaia cookie. 377 PerformLogoutAllAccountsAction(); 378 gaia_accounts_.clear(); 379 } 380 381 // Create a list of accounts that need to be added to the gaia cookie. 382 // The primary account must be first to make sure it becomes the default 383 // account in the case where chrome is completely rebuilding the cookie. 384 add_to_cookie_.push_back(primary_account_); 385 for (size_t i = 0; i < chrome_accounts_.size(); ++i) { 386 if (chrome_accounts_[i] != primary_account_) 387 add_to_cookie_.push_back(chrome_accounts_[i]); 388 } 389 390 // For each account known to chrome, PerformMergeAction() if the account is 391 // not already in the cookie jar or its state is invalid, or signal merge 392 // completed otherwise. Make a copy of |add_to_cookie_| since calls to 393 // SignalComplete() will change the array. 394 std::vector<std::string> add_to_cookie_copy = add_to_cookie_; 395 int added_to_cookie = 0; 396 bool external_cc_result_completed = 397 !merge_session_helper_.StillFetchingExternalCcResult(); 398 for (size_t i = 0; i < add_to_cookie_copy.size(); ++i) { 399 if (gaia_accounts_.end() != 400 std::find_if(gaia_accounts_.begin(), 401 gaia_accounts_.end(), 402 std::bind1st(EmailEqualToFunc(), 403 std::make_pair(add_to_cookie_copy[i], 404 true)))) { 405 merge_session_helper_.SignalComplete( 406 add_to_cookie_copy[i], 407 GoogleServiceAuthError::AuthErrorNone()); 408 } else { 409 PerformMergeAction(add_to_cookie_copy[i]); 410 if (original_gaia_accounts.end() == 411 std::find_if(original_gaia_accounts.begin(), 412 original_gaia_accounts.end(), 413 std::bind1st(EmailEqualToFunc(), 414 std::make_pair(add_to_cookie_copy[i], 415 true)))) { 416 added_to_cookie++; 417 } 418 } 419 } 420 421 // Log whether the external connection checks were completed when we tried 422 // to add the accounts to the cookie. 423 if (rebuild_cookie || added_to_cookie > 0) 424 signin_metrics::LogExternalCcResultFetches(external_cc_result_completed); 425 426 signin_metrics::LogSigninAccountReconciliation(chrome_accounts_.size(), 427 added_to_cookie, 428 removed_from_cookie, 429 are_primaries_equal, 430 first_execution_, 431 number_gaia_accounts); 432 first_execution_ = false; 433 CalculateIfReconcileIsDone(); 434 ScheduleStartReconcileIfChromeAccountsChanged(); 435} 436 437void AccountReconcilor::AbortReconcile() { 438 VLOG(1) << "AccountReconcilor::AbortReconcile: we'll try again later"; 439 add_to_cookie_.clear(); 440 CalculateIfReconcileIsDone(); 441} 442 443void AccountReconcilor::CalculateIfReconcileIsDone() { 444 is_reconcile_started_ = !add_to_cookie_.empty(); 445 if (!is_reconcile_started_) 446 VLOG(1) << "AccountReconcilor::CalculateIfReconcileIsDone: done"; 447} 448 449void AccountReconcilor::ScheduleStartReconcileIfChromeAccountsChanged() { 450 if (is_reconcile_started_) 451 return; 452 453 // Start a reconcile as the token accounts have changed. 454 VLOG(1) << "AccountReconcilor::StartReconcileIfChromeAccountsChanged"; 455 std::vector<std::string> reconciled_accounts(chrome_accounts_); 456 std::vector<std::string> new_chrome_accounts(token_service_->GetAccounts()); 457 std::sort(reconciled_accounts.begin(), reconciled_accounts.end()); 458 std::sort(new_chrome_accounts.begin(), new_chrome_accounts.end()); 459 if (reconciled_accounts != new_chrome_accounts) { 460 base::MessageLoop::current()->PostTask( 461 FROM_HERE, 462 base::Bind(&AccountReconcilor::StartReconcile, base::Unretained(this))); 463 } 464} 465 466// Remove the account from the list that is being merged. 467bool AccountReconcilor::MarkAccountAsAddedToCookie( 468 const std::string& account_id) { 469 for (std::vector<std::string>::iterator i = add_to_cookie_.begin(); 470 i != add_to_cookie_.end(); 471 ++i) { 472 if (account_id == *i) { 473 add_to_cookie_.erase(i); 474 return true; 475 } 476 } 477 return false; 478} 479 480void AccountReconcilor::MergeSessionCompleted( 481 const std::string& account_id, 482 const GoogleServiceAuthError& error) { 483 VLOG(1) << "AccountReconcilor::MergeSessionCompleted: account_id=" 484 << account_id << " error=" << error.ToString(); 485 486 if (MarkAccountAsAddedToCookie(account_id)) { 487 CalculateIfReconcileIsDone(); 488 ScheduleStartReconcileIfChromeAccountsChanged(); 489 } 490} 491