buffet: Handle cloud request failures due to device deleted on cloud

When making cloud request and receiving "authorization failed" error,
we are refreshing the access token. If that fails with "invalid_grant",
we are certain that the device's robot account has been deleted, so
we should treat the device as no longer registered with the cloud, and
delete its cached credentials.

This CL adds OnDeviceDeleted() notification which wipes out the device
registration information. This notification is called when refreshing
the access token fails with "invalid_grant".

A follow-up CL will add handling for DEVICE_DELETED XMPP notification.

BUG=brillo:1215
TEST=`FEATURES=test emerge-link buffet`
     `test_that -b link 100.96.49.59 e:buffet_.*`
CQ-DEPEND=CL:281664

Change-Id: I220c286dbd286dd036d7664a4d14e9d761d70f28
Reviewed-on: https://chromium-review.googlesource.com/281393
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Trybot-Ready: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index e27b35d..b76c79e 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -169,7 +169,7 @@
 }
 
 void DeviceRegistrationInfo::Start() {
-  if (HaveRegistrationCredentials(nullptr)) {
+  if (HaveRegistrationCredentials()) {
     StartNotificationChannel();
     // Wait a significant amount of time for local daemons to publish their
     // state to Buffet before publishing it to the cloud.
@@ -201,11 +201,15 @@
       later);
 }
 
-bool DeviceRegistrationInfo::HaveRegistrationCredentials(
-    chromeos::ErrorPtr* error) {
-  const bool have_credentials = !config_->refresh_token().empty() &&
-                                !config_->device_id().empty() &&
-                                !config_->robot_account().empty();
+bool DeviceRegistrationInfo::HaveRegistrationCredentials() const {
+  return !config_->refresh_token().empty() &&
+         !config_->device_id().empty() &&
+         !config_->robot_account().empty();
+}
+
+bool DeviceRegistrationInfo::VerifyRegistrationCredentials(
+    chromeos::ErrorPtr* error) const {
+  const bool have_credentials = HaveRegistrationCredentials();
 
   VLOG(1) << "Device registration record "
           << ((have_credentials) ? "found" : "not found.");
@@ -245,6 +249,13 @@
     const base::Closure& success_callback,
     const CloudRequestErrorCallback& error_callback) {
   LOG(INFO) << "Refreshing access token.";
+
+  chromeos::ErrorPtr error;
+  if (!VerifyRegistrationCredentials(&error)) {
+    error_callback.Run(error.get());
+    return;
+  }
+
   // Make a shared pointer to |error_callback| since we are going to share this
   // callback with both success and error callbacks for PostFormData() and if
   // |error_callback| has any move-only types, one of the copies will be bad.
@@ -314,6 +325,8 @@
   if (notification_channel_starting_)
     return;
 
+  LOG(INFO) << "Starting notification channel";
+
   // If no MessageLoop assume we're in unittests.
   if (!base::MessageLoop::current()) {
     LOG(INFO) << "No MessageLoop, not starting notification channel";
@@ -532,7 +545,7 @@
   VLOG(1) << "Sending cloud request '" << data->method << "' to '" << data->url
           << "' with request body '" << data->body << "'";
   chromeos::ErrorPtr error;
-  if (!HaveRegistrationCredentials(&error)) {
+  if (!VerifyRegistrationCredentials(&error)) {
     data->error_callback.Run(error.get());
     return;
   }
@@ -632,13 +645,15 @@
 void DeviceRegistrationInfo::OnAccessTokenError(
     const std::shared_ptr<const CloudRequestData>& data,
     const chromeos::Error* error) {
+  if (error->HasError(buffet::kErrorDomainOAuth2, "invalid_grant"))
+    MarkDeviceUnregistered();
   data->error_callback.Run(error);
 }
 
 void DeviceRegistrationInfo::StartDevice(
     chromeos::ErrorPtr* error,
     const base::TimeDelta& retry_delay) {
-  if (!HaveRegistrationCredentials(error))
+  if (!VerifyRegistrationCredentials(error))
     return;
   auto handle_start_device_failure_cb = base::Bind(
       &IgnoreCloudErrorWithCallback,
@@ -671,7 +686,7 @@
   change.set_location(location);
   change.Commit();
 
-  if (HaveRegistrationCredentials(nullptr)) {
+  if (HaveRegistrationCredentials()) {
     UpdateDeviceResource(base::Bind(&base::DoNothing),
                          base::Bind(&IgnoreCloudError));
   }
@@ -705,7 +720,7 @@
     const std::string& oauth_url,
     const std::string& service_url,
     chromeos::ErrorPtr* error) {
-  if (HaveRegistrationCredentials(nullptr)) {
+  if (HaveRegistrationCredentials()) {
     chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainBuffet,
                            "already_registered",
                            "Unable to change config for registered device");
@@ -956,7 +971,7 @@
 
 void DeviceRegistrationInfo::OnCommandDefsChanged() {
   VLOG(1) << "CommandDefinitionChanged notification received";
-  if (!HaveRegistrationCredentials(nullptr))
+  if (!HaveRegistrationCredentials())
     return;
 
   UpdateDeviceResource(base::Bind(&base::DoNothing),
@@ -965,7 +980,7 @@
 
 void DeviceRegistrationInfo::OnStateChanged() {
   VLOG(1) << "StateChanged notification received";
-  if (!HaveRegistrationCredentials(nullptr))
+  if (!HaveRegistrationCredentials())
     return;
 
   // TODO(vitalybuka): Integrate BackoffEntry.
@@ -985,6 +1000,9 @@
 
 void DeviceRegistrationInfo::OnDisconnected() {
   LOG(INFO) << "Notification channel disconnected";
+  if (!HaveRegistrationCredentials())
+    return;
+
   pull_channel_->UpdatePullInterval(config_->polling_period());
   current_notification_channel_ = pull_channel_.get();
   UpdateDeviceResource(base::Bind(&base::DoNothing),
@@ -994,8 +1012,13 @@
 void DeviceRegistrationInfo::OnPermanentFailure() {
   LOG(ERROR) << "Failed to establish notification channel.";
   notification_channel_starting_ = false;
-  RefreshAccessToken(base::Bind(&base::DoNothing),
-                     base::Bind(&IgnoreCloudError));
+  auto mark_unregistered =
+      base::Bind(&DeviceRegistrationInfo::MarkDeviceUnregistered, AsWeakPtr());
+  auto error_callback = [mark_unregistered](const chromeos::Error* error) {
+    if (error->HasError(buffet::kErrorDomainOAuth2, "invalid_grant"))
+      mark_unregistered.Run();
+  };
+  RefreshAccessToken(base::Bind(&base::DoNothing), base::Bind(error_callback));
 }
 
 void DeviceRegistrationInfo::OnCommandCreated(
@@ -1014,5 +1037,27 @@
                 base::Bind(&IgnoreCloudError));
 }
 
+void DeviceRegistrationInfo::MarkDeviceUnregistered() {
+  if (!HaveRegistrationCredentials())
+    return;
+
+  LOG(INFO) << "Device is unregistered from the cloud. Deleting credentials";
+  BuffetConfig::Transaction change{config_.get()};
+  change.set_device_id("");
+  change.set_robot_account("");
+  change.set_refresh_token("");
+  change.Commit();
+
+  current_notification_channel_ = nullptr;
+  if (primary_notification_channel_) {
+    primary_notification_channel_->Stop();
+    primary_notification_channel_.reset();
+  }
+  if (pull_channel_) {
+    pull_channel_->Stop();
+    pull_channel_.reset();
+  }
+  notification_channel_starting_ = false;
+}
 
 }  // namespace buffet