blob: e6e241cd8a72f86ea85504873e5bf73e043b6d77 [file] [log] [blame]
// Copyright (c) 2012 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.
#include "chrome/browser/chromeos/policy/auto_enrollment_client.h"
#include "base/bind.h"
#include "base/guid.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/message_loop/message_loop_proxy.h"
#include "base/metrics/histogram.h"
#include "base/metrics/sparse_histogram.h"
#include "base/prefs/pref_registry_simple.h"
#include "base/prefs/pref_service.h"
#include "base/prefs/scoped_user_pref_update.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/policy/server_backed_device_state.h"
#include "chrome/common/chrome_content_client.h"
#include "chrome/common/pref_names.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "components/policy/core/common/cloud/system_policy_request_context.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/sha2.h"
#include "net/url_request/url_request_context_getter.h"
#include "policy/proto/device_management_backend.pb.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace em = enterprise_management;
namespace policy {
namespace {
// UMA histogram names.
const char kUMAProtocolTime[] = "Enterprise.AutoEnrollmentProtocolTime";
const char kUMAExtraTime[] = "Enterprise.AutoEnrollmentExtraTime";
const char kUMARequestStatus[] = "Enterprise.AutoEnrollmentRequestStatus";
const char kUMANetworkErrorCode[] =
"Enterprise.AutoEnrollmentRequestNetworkErrorCode";
// Returns the power of the next power-of-2 starting at |value|.
int NextPowerOf2(int64 value) {
for (int i = 0; i <= AutoEnrollmentClient::kMaximumPower; ++i) {
if ((GG_INT64_C(1) << i) >= value)
return i;
}
// No other value can be represented in an int64.
return AutoEnrollmentClient::kMaximumPower + 1;
}
// Sets or clears a value in a dictionary.
void UpdateDict(base::DictionaryValue* dict,
const char* pref_path,
bool set_or_clear,
base::Value* value) {
scoped_ptr<base::Value> scoped_value(value);
if (set_or_clear)
dict->Set(pref_path, scoped_value.release());
else
dict->Remove(pref_path, NULL);
}
// Converts a restore mode enum value from the DM protocol into the
// corresponding prefs string constant.
std::string ConvertRestoreMode(
em::DeviceStateRetrievalResponse::RestoreMode restore_mode) {
switch (restore_mode) {
case em::DeviceStateRetrievalResponse::RESTORE_MODE_NONE:
return std::string();
case em::DeviceStateRetrievalResponse::RESTORE_MODE_REENROLLMENT_REQUESTED:
return kDeviceStateRestoreModeReEnrollmentRequested;
case em::DeviceStateRetrievalResponse::RESTORE_MODE_REENROLLMENT_ENFORCED:
return kDeviceStateRestoreModeReEnrollmentEnforced;
}
// Return is required to avoid compiler warning.
NOTREACHED() << "Bad restore mode " << restore_mode;
return std::string();
}
} // namespace
AutoEnrollmentClient::AutoEnrollmentClient(
const ProgressCallback& callback,
DeviceManagementService* service,
PrefService* local_state,
scoped_refptr<net::URLRequestContextGetter> system_request_context,
const std::string& server_backed_state_key,
bool retrieve_device_state,
int power_initial,
int power_limit)
: progress_callback_(callback),
state_(AUTO_ENROLLMENT_STATE_IDLE),
has_server_state_(false),
device_state_available_(false),
device_id_(base::GenerateGUID()),
server_backed_state_key_(server_backed_state_key),
retrieve_device_state_(retrieve_device_state),
current_power_(power_initial),
power_limit_(power_limit),
modulus_updates_received_(0),
device_management_service_(service),
local_state_(local_state) {
request_context_ = new SystemPolicyRequestContext(
system_request_context, GetUserAgent());
DCHECK_LE(current_power_, power_limit_);
DCHECK(!progress_callback_.is_null());
if (!server_backed_state_key_.empty()) {
server_backed_state_key_hash_ =
crypto::SHA256HashString(server_backed_state_key_);
}
}
AutoEnrollmentClient::~AutoEnrollmentClient() {
net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
}
// static
void AutoEnrollmentClient::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kShouldAutoEnroll, false);
registry->RegisterIntegerPref(prefs::kAutoEnrollmentPowerLimit, -1);
}
// static
void AutoEnrollmentClient::CancelAutoEnrollment() {
PrefService* local_state = g_browser_process->local_state();
local_state->SetBoolean(prefs::kShouldAutoEnroll, false);
local_state->ClearPref(prefs::kServerBackedDeviceState);
local_state->CommitPendingWrite();
}
void AutoEnrollmentClient::Start() {
// (Re-)register the network change observer.
net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
// Drop the previous job and reset state.
request_job_.reset();
state_ = AUTO_ENROLLMENT_STATE_PENDING;
time_start_ = base::Time::Now();
modulus_updates_received_ = 0;
has_server_state_ = false;
device_state_available_ = false;
NextStep();
}
void AutoEnrollmentClient::Retry() {
RetryStep();
}
void AutoEnrollmentClient::CancelAndDeleteSoon() {
if (time_start_.is_null() || !request_job_) {
// The client isn't running, just delete it.
delete this;
} else {
// Client still running, but our owner isn't interested in the result
// anymore. Wait until the protocol completes to measure the extra time
// needed.
time_extra_start_ = base::Time::Now();
progress_callback_.Reset();
}
}
void AutoEnrollmentClient::OnNetworkChanged(
net::NetworkChangeNotifier::ConnectionType type) {
if (type != net::NetworkChangeNotifier::CONNECTION_NONE &&
!progress_callback_.is_null()) {
RetryStep();
}
}
bool AutoEnrollmentClient::GetCachedDecision() {
const PrefService::Preference* has_server_state_pref =
local_state_->FindPreference(prefs::kShouldAutoEnroll);
const PrefService::Preference* previous_limit_pref =
local_state_->FindPreference(prefs::kAutoEnrollmentPowerLimit);
bool has_server_state = false;
int previous_limit = -1;
if (!has_server_state_pref ||
has_server_state_pref->IsDefaultValue() ||
!has_server_state_pref->GetValue()->GetAsBoolean(&has_server_state) ||
!previous_limit_pref ||
previous_limit_pref->IsDefaultValue() ||
!previous_limit_pref->GetValue()->GetAsInteger(&previous_limit) ||
power_limit_ > previous_limit) {
return false;
}
has_server_state_ = has_server_state;
return true;
}
bool AutoEnrollmentClient::RetryStep() {
// If there is a pending request job, let it finish.
if (request_job_)
return true;
if (GetCachedDecision()) {
// The bucket download check has completed already. If it came back
// positive, then device state should be (re-)downloaded.
if (has_server_state_) {
if (retrieve_device_state_ && !device_state_available_ &&
SendDeviceStateRequest()) {
return true;
}
}
} else {
// Start bucket download.
if (SendBucketDownloadRequest())
return true;
}
return false;
}
void AutoEnrollmentClient::ReportProgress(AutoEnrollmentState state) {
state_ = state;
if (progress_callback_.is_null()) {
base::MessageLoopProxy::current()->DeleteSoon(FROM_HERE, this);
} else {
progress_callback_.Run(state_);
}
}
void AutoEnrollmentClient::NextStep() {
if (!RetryStep()) {
// Protocol finished successfully, report result.
bool trigger_enrollment = false;
if (retrieve_device_state_) {
const base::DictionaryValue* device_state_dict =
local_state_->GetDictionary(prefs::kServerBackedDeviceState);
std::string restore_mode;
device_state_dict->GetString(kDeviceStateRestoreMode, &restore_mode);
trigger_enrollment =
(restore_mode == kDeviceStateRestoreModeReEnrollmentRequested ||
restore_mode == kDeviceStateRestoreModeReEnrollmentEnforced);
} else {
trigger_enrollment = has_server_state_;
}
ReportProgress(trigger_enrollment ? AUTO_ENROLLMENT_STATE_TRIGGER_ENROLLMENT
: AUTO_ENROLLMENT_STATE_NO_ENROLLMENT);
}
}
bool AutoEnrollmentClient::SendBucketDownloadRequest() {
if (server_backed_state_key_hash_.empty())
return false;
// Only power-of-2 moduli are supported for now. These are computed by taking
// the lower |current_power_| bits of the hash.
uint64 remainder = 0;
for (int i = 0; 8 * i < current_power_; ++i) {
uint64 byte = server_backed_state_key_hash_[31 - i] & 0xff;
remainder = remainder | (byte << (8 * i));
}
remainder = remainder & ((GG_UINT64_C(1) << current_power_) - 1);
ReportProgress(AUTO_ENROLLMENT_STATE_PENDING);
request_job_.reset(
device_management_service_->CreateJob(
DeviceManagementRequestJob::TYPE_AUTO_ENROLLMENT,
request_context_.get()));
request_job_->SetClientID(device_id_);
em::DeviceAutoEnrollmentRequest* request =
request_job_->GetRequest()->mutable_auto_enrollment_request();
request->set_remainder(remainder);
request->set_modulus(GG_INT64_C(1) << current_power_);
request_job_->Start(
base::Bind(&AutoEnrollmentClient::HandleRequestCompletion,
base::Unretained(this),
&AutoEnrollmentClient::OnBucketDownloadRequestCompletion));
return true;
}
bool AutoEnrollmentClient::SendDeviceStateRequest() {
ReportProgress(AUTO_ENROLLMENT_STATE_PENDING);
request_job_.reset(
device_management_service_->CreateJob(
DeviceManagementRequestJob::TYPE_DEVICE_STATE_RETRIEVAL,
request_context_.get()));
request_job_->SetClientID(device_id_);
em::DeviceStateRetrievalRequest* request =
request_job_->GetRequest()->mutable_device_state_retrieval_request();
request->set_server_backed_state_key(server_backed_state_key_);
request_job_->Start(
base::Bind(&AutoEnrollmentClient::HandleRequestCompletion,
base::Unretained(this),
&AutoEnrollmentClient::OnDeviceStateRequestCompletion));
return true;
}
void AutoEnrollmentClient::HandleRequestCompletion(
RequestCompletionHandler handler,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
UMA_HISTOGRAM_SPARSE_SLOWLY(kUMARequestStatus, status);
if (status != DM_STATUS_SUCCESS) {
LOG(ERROR) << "Auto enrollment error: " << status;
if (status == DM_STATUS_REQUEST_FAILED)
UMA_HISTOGRAM_SPARSE_SLOWLY(kUMANetworkErrorCode, -net_error);
request_job_.reset();
// Abort if CancelAndDeleteSoon has been called meanwhile.
if (progress_callback_.is_null()) {
base::MessageLoopProxy::current()->DeleteSoon(FROM_HERE, this);
} else {
ReportProgress(status == DM_STATUS_REQUEST_FAILED
? AUTO_ENROLLMENT_STATE_CONNECTION_ERROR
: AUTO_ENROLLMENT_STATE_SERVER_ERROR);
}
return;
}
bool progress = (this->*handler)(status, net_error, response);
request_job_.reset();
if (progress)
NextStep();
else
ReportProgress(AUTO_ENROLLMENT_STATE_SERVER_ERROR);
}
bool AutoEnrollmentClient::OnBucketDownloadRequestCompletion(
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
bool progress = false;
const em::DeviceAutoEnrollmentResponse& enrollment_response =
response.auto_enrollment_response();
if (!response.has_auto_enrollment_response()) {
LOG(ERROR) << "Server failed to provide auto-enrollment response.";
} else if (enrollment_response.has_expected_modulus()) {
// Server is asking us to retry with a different modulus.
modulus_updates_received_++;
int64 modulus = enrollment_response.expected_modulus();
int power = NextPowerOf2(modulus);
if ((GG_INT64_C(1) << power) != modulus) {
LOG(WARNING) << "Auto enrollment: the server didn't ask for a power-of-2 "
<< "modulus. Using the closest power-of-2 instead "
<< "(" << modulus << " vs 2^" << power << ")";
}
if (modulus_updates_received_ >= 2) {
LOG(ERROR) << "Auto enrollment error: already retried with an updated "
<< "modulus but the server asked for a new one again: "
<< power;
} else if (power > power_limit_) {
LOG(ERROR) << "Auto enrollment error: the server asked for a larger "
<< "modulus than the client accepts (" << power << " vs "
<< power_limit_ << ").";
} else {
// Retry at most once with the modulus that the server requested.
if (power <= current_power_) {
LOG(WARNING) << "Auto enrollment: the server asked to use a modulus ("
<< power << ") that isn't larger than the first used ("
<< current_power_ << "). Retrying anyway.";
}
// Remember this value, so that eventual retries start with the correct
// modulus.
current_power_ = power;
return true;
}
} else {
// Server should have sent down a list of hashes to try.
has_server_state_ = IsIdHashInProtobuf(enrollment_response.hash());
// Cache the current decision in local_state, so that it is reused in case
// the device reboots before enrolling.
local_state_->SetBoolean(prefs::kShouldAutoEnroll, has_server_state_);
local_state_->SetInteger(prefs::kAutoEnrollmentPowerLimit, power_limit_);
local_state_->CommitPendingWrite();
VLOG(1) << "Auto enrollment check complete, has_server_state_ = "
<< has_server_state_;
progress = true;
}
// Bucket download done, update UMA.
UpdateBucketDownloadTimingHistograms();
return progress;
}
bool AutoEnrollmentClient::OnDeviceStateRequestCompletion(
DeviceManagementStatus status,
int net_error,
const enterprise_management::DeviceManagementResponse& response) {
bool progress = false;
if (!response.has_device_state_retrieval_response()) {
LOG(ERROR) << "Server failed to provide auto-enrollment response.";
} else {
const em::DeviceStateRetrievalResponse& state_response =
response.device_state_retrieval_response();
{
DictionaryPrefUpdate dict(local_state_, prefs::kServerBackedDeviceState);
UpdateDict(dict.Get(),
kDeviceStateManagementDomain,
state_response.has_management_domain(),
new base::StringValue(state_response.management_domain()));
std::string restore_mode =
ConvertRestoreMode(state_response.restore_mode());
UpdateDict(dict.Get(),
kDeviceStateRestoreMode,
!restore_mode.empty(),
new base::StringValue(restore_mode));
UpdateDict(dict.Get(),
kDeviceStateDisabled,
true /* set_or_clear */,
new base::FundamentalValue(
state_response.has_disabled_state()));
UpdateDict(dict.Get(),
kDeviceStateDisabledMessage,
state_response.has_disabled_state(),
new base::StringValue(
state_response.disabled_state().message()));
}
local_state_->CommitPendingWrite();
device_state_available_ = true;
progress = true;
}
return progress;
}
bool AutoEnrollmentClient::IsIdHashInProtobuf(
const google::protobuf::RepeatedPtrField<std::string>& hashes) {
for (int i = 0; i < hashes.size(); ++i) {
if (hashes.Get(i) == server_backed_state_key_hash_)
return true;
}
return false;
}
void AutoEnrollmentClient::UpdateBucketDownloadTimingHistograms() {
// The minimum time can't be 0, must be at least 1.
static const base::TimeDelta kMin = base::TimeDelta::FromMilliseconds(1);
static const base::TimeDelta kMax = base::TimeDelta::FromMinutes(5);
// However, 0 can still be sampled.
static const base::TimeDelta kZero = base::TimeDelta::FromMilliseconds(0);
static const int kBuckets = 50;
base::Time now = base::Time::Now();
if (!time_start_.is_null()) {
base::TimeDelta delta = now - time_start_;
UMA_HISTOGRAM_CUSTOM_TIMES(kUMAProtocolTime, delta, kMin, kMax, kBuckets);
}
base::TimeDelta delta = kZero;
if (!time_extra_start_.is_null())
delta = now - time_extra_start_;
// This samples |kZero| when there was no need for extra time, so that we can
// measure the ratio of users that succeeded without needing a delay to the
// total users going through OOBE.
UMA_HISTOGRAM_CUSTOM_TIMES(kUMAExtraTime, delta, kMin, kMax, kBuckets);
}
} // namespace policy