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