blob: e6795eabf22ca6c0bfea0ca46c08ae1f7041d48e [file] [log] [blame]
// Copyright 2013 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/policy/cloud/cloud_policy_invalidator.h"
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/hash/hash.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/strcat.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/policy/cloud/policy_invalidation_util.h"
#include "chrome/common/chrome_features.h"
#include "components/invalidation/public/invalidation_service.h"
#include "components/invalidation/public/invalidation_util.h"
#include "components/invalidation/public/topic_invalidation_map.h"
#include "components/policy/core/common/cloud/cloud_policy_client.h"
#include "components/policy/core/common/cloud/cloud_policy_refresh_scheduler.h"
#include "components/policy/core/common/cloud/enterprise_metrics.h"
#include "components/policy/policy_constants.h"
namespace policy {
namespace {
MetricPolicyRefresh GetPolicyRefreshMetric(bool invalidations_enabled,
bool policy_changed,
bool invalidated) {
if (policy_changed) {
if (invalidated)
return METRIC_POLICY_REFRESH_INVALIDATED_CHANGED;
if (invalidations_enabled)
return METRIC_POLICY_REFRESH_CHANGED;
return METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS;
}
if (invalidated)
return METRIC_POLICY_REFRESH_INVALIDATED_UNCHANGED;
return METRIC_POLICY_REFRESH_UNCHANGED;
}
PolicyInvalidationType GetInvalidationMetric(bool is_missing_payload,
bool is_expired) {
if (is_expired) {
if (is_missing_payload)
return POLICY_INVALIDATION_TYPE_NO_PAYLOAD_EXPIRED;
return POLICY_INVALIDATION_TYPE_EXPIRED;
}
if (is_missing_payload)
return POLICY_INVALIDATION_TYPE_NO_PAYLOAD;
return POLICY_INVALIDATION_TYPE_NORMAL;
}
void RecordPolicyRefreshMetric(PolicyInvalidationScope scope,
bool invalidations_enabled,
bool policy_changed,
bool invalidated) {
const MetricPolicyRefresh metric_policy_refresh = GetPolicyRefreshMetric(
invalidations_enabled, policy_changed, invalidated);
base::UmaHistogramEnumeration(
CloudPolicyInvalidator::GetPolicyRefreshMetricName(scope),
metric_policy_refresh, METRIC_POLICY_REFRESH_SIZE);
base::UmaHistogramEnumeration(
CloudPolicyInvalidator::GetPolicyRefreshFcmMetricName(scope),
metric_policy_refresh, METRIC_POLICY_REFRESH_SIZE);
}
void RecordPolicyInvalidationMetric(PolicyInvalidationScope scope,
bool is_expired,
bool is_missing_payload) {
const PolicyInvalidationType policy_invalidation_type =
GetInvalidationMetric(is_missing_payload, is_expired);
base::UmaHistogramEnumeration(
CloudPolicyInvalidator::GetPolicyInvalidationMetricName(scope),
policy_invalidation_type, POLICY_INVALIDATION_TYPE_SIZE);
base::UmaHistogramEnumeration(
CloudPolicyInvalidator::GetPolicyInvalidationFcmMetricName(scope),
policy_invalidation_type, POLICY_INVALIDATION_TYPE_SIZE);
}
std::string ComposeOwnerName(PolicyInvalidationScope scope,
const std::string& device_local_account_id) {
if (!base::FeatureList::IsEnabled(features::kInvalidatorUniqueOwnerName)) {
return "Cloud";
}
switch (scope) {
case PolicyInvalidationScope::kUser:
return "CloudPolicy.User";
case PolicyInvalidationScope::kDevice:
return "CloudPolicy.Device";
case PolicyInvalidationScope::kDeviceLocalAccount:
DCHECK(!device_local_account_id.empty());
return base::StrCat(
{"CloudPolicy.DeviceLocalAccount.", device_local_account_id});
case PolicyInvalidationScope::kCBCM:
return "CloudPolicy.CBCM";
}
}
} // namespace
const int CloudPolicyInvalidator::kMissingPayloadDelay = 5;
const int CloudPolicyInvalidator::kMaxFetchDelayDefault = 10000;
const int CloudPolicyInvalidator::kMaxFetchDelayMin = 1000;
const int CloudPolicyInvalidator::kMaxFetchDelayMax = 300000;
const int CloudPolicyInvalidator::kInvalidationGracePeriod = 10;
// static
const char* CloudPolicyInvalidator::GetPolicyRefreshMetricName(
PolicyInvalidationScope scope) {
switch (scope) {
case PolicyInvalidationScope::kUser:
return kMetricUserPolicyRefresh;
case PolicyInvalidationScope::kDevice:
return kMetricDevicePolicyRefresh;
case PolicyInvalidationScope::kDeviceLocalAccount:
return kMetricDeviceLocalAccountPolicyRefresh;
case PolicyInvalidationScope::kCBCM:
return kMetricCBCMPolicyRefresh;
}
}
// static
const char* CloudPolicyInvalidator::GetPolicyRefreshFcmMetricName(
PolicyInvalidationScope scope) {
switch (scope) {
case PolicyInvalidationScope::kUser:
return kMetricUserPolicyRefreshFcm;
case PolicyInvalidationScope::kDevice:
return kMetricDevicePolicyRefreshFcm;
case PolicyInvalidationScope::kDeviceLocalAccount:
return kMetricDeviceLocalAccountPolicyRefreshFcm;
case PolicyInvalidationScope::kCBCM:
return kMetricCBCMPolicyRefreshFcm;
}
}
// static
const char* CloudPolicyInvalidator::GetPolicyInvalidationMetricName(
PolicyInvalidationScope scope) {
switch (scope) {
case PolicyInvalidationScope::kUser:
return kMetricUserPolicyInvalidations;
case PolicyInvalidationScope::kDevice:
return kMetricDevicePolicyInvalidations;
case PolicyInvalidationScope::kDeviceLocalAccount:
return kMetricDeviceLocalAccountPolicyInvalidations;
case PolicyInvalidationScope::kCBCM:
return kMetricCBCMPolicyInvalidations;
}
}
// static
const char* CloudPolicyInvalidator::GetPolicyInvalidationFcmMetricName(
PolicyInvalidationScope scope) {
switch (scope) {
case PolicyInvalidationScope::kUser:
return kMetricUserPolicyInvalidationsFcm;
case PolicyInvalidationScope::kDevice:
return kMetricDevicePolicyInvalidationsFcm;
case PolicyInvalidationScope::kDeviceLocalAccount:
return kMetricDeviceLocalAccountPolicyInvalidationsFcm;
case PolicyInvalidationScope::kCBCM:
return kMetricCBCMPolicyInvalidationsFcm;
}
}
CloudPolicyInvalidator::CloudPolicyInvalidator(
PolicyInvalidationScope scope,
CloudPolicyCore* core,
const scoped_refptr<base::SequencedTaskRunner>& task_runner,
base::Clock* clock,
int64_t highest_handled_invalidation_version)
: CloudPolicyInvalidator(scope,
core,
task_runner,
clock,
highest_handled_invalidation_version,
/*device_local_account_id=*/"") {}
CloudPolicyInvalidator::CloudPolicyInvalidator(
PolicyInvalidationScope scope,
CloudPolicyCore* core,
const scoped_refptr<base::SequencedTaskRunner>& task_runner,
base::Clock* clock,
int64_t highest_handled_invalidation_version,
const std::string& device_local_account_id)
: state_(UNINITIALIZED),
scope_(scope),
owner_name_(ComposeOwnerName(scope, device_local_account_id)),
core_(core),
task_runner_(task_runner),
clock_(clock),
invalidation_service_(nullptr),
invalidations_enabled_(false),
invalidation_service_enabled_(false),
is_registered_(false),
invalid_(false),
invalidation_version_(0),
unknown_version_invalidation_count_(0),
highest_handled_invalidation_version_(
highest_handled_invalidation_version),
max_fetch_delay_(kMaxFetchDelayDefault),
policy_hash_value_(0) {
DCHECK(core);
DCHECK(task_runner.get());
// |highest_handled_invalidation_version_| indicates the highest actual
// invalidation version handled. Since actual invalidations can have only
// positive versions, this member may be zero (no versioned invalidation
// handled yet) or positive. Negative values are not allowed:
//
// Negative version numbers are used internally by CloudPolicyInvalidator to
// keep track of unversioned invalidations. When such an invalidation is
// handled, |highest_handled_invalidation_version_| remains unchanged and does
// not become negative.
DCHECK_LE(0, highest_handled_invalidation_version_);
}
CloudPolicyInvalidator::~CloudPolicyInvalidator() {
DCHECK(state_ == SHUT_DOWN);
}
void CloudPolicyInvalidator::Initialize(
invalidation::InvalidationService* invalidation_service) {
DCHECK(state_ == UNINITIALIZED);
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(invalidation_service);
invalidation_service_ = invalidation_service;
state_ = STOPPED;
core_->AddObserver(this);
if (core_->refresh_scheduler())
OnRefreshSchedulerStarted(core_);
}
void CloudPolicyInvalidator::Shutdown() {
DCHECK(state_ != SHUT_DOWN);
DCHECK(thread_checker_.CalledOnValidThread());
if (state_ == STARTED) {
if (is_registered_)
invalidation_service_->UnregisterInvalidationHandler(this);
core_->store()->RemoveObserver(this);
weak_factory_.InvalidateWeakPtrs();
}
if (state_ != UNINITIALIZED)
core_->RemoveObserver(this);
state_ = SHUT_DOWN;
}
void CloudPolicyInvalidator::OnInvalidatorStateChange(
invalidation::InvalidatorState state) {
DCHECK(state_ == STARTED);
DCHECK(thread_checker_.CalledOnValidThread());
invalidation_service_enabled_ = state == invalidation::INVALIDATIONS_ENABLED;
UpdateInvalidationsEnabled();
}
void CloudPolicyInvalidator::OnIncomingInvalidation(
const invalidation::TopicInvalidationMap& invalidation_map) {
DCHECK(state_ == STARTED);
DCHECK(thread_checker_.CalledOnValidThread());
const invalidation::SingleTopicInvalidationSet& list =
invalidation_map.ForTopic(topic_);
if (list.IsEmpty()) {
NOTREACHED();
return;
}
// Acknowledge all except the invalidation with the highest version.
auto it = list.rbegin();
++it;
for ( ; it != list.rend(); ++it) {
it->Acknowledge();
}
// Handle the highest version invalidation.
HandleInvalidation(list.back());
}
std::string CloudPolicyInvalidator::GetOwnerName() const {
return owner_name_;
}
bool CloudPolicyInvalidator::IsPublicTopic(
const invalidation::Topic& topic) const {
return IsPublicInvalidationTopic(topic);
}
void CloudPolicyInvalidator::OnCoreConnected(CloudPolicyCore* core) {}
void CloudPolicyInvalidator::OnRefreshSchedulerStarted(CloudPolicyCore* core) {
DCHECK(state_ == STOPPED);
DCHECK(thread_checker_.CalledOnValidThread());
state_ = STARTED;
OnStoreLoaded(core_->store());
core_->store()->AddObserver(this);
}
void CloudPolicyInvalidator::OnCoreDisconnecting(CloudPolicyCore* core) {
DCHECK(state_ == STARTED || state_ == STOPPED);
DCHECK(thread_checker_.CalledOnValidThread());
if (state_ == STARTED) {
Unregister();
core_->store()->RemoveObserver(this);
state_ = STOPPED;
}
}
void CloudPolicyInvalidator::OnStoreLoaded(CloudPolicyStore* store) {
DCHECK(state_ == STARTED);
DCHECK(thread_checker_.CalledOnValidThread());
bool policy_changed = IsPolicyChanged(store->policy());
if (is_registered_) {
const int64_t store_invalidation_version = store->invalidation_version();
// Whether the refresh was caused by invalidation.
const bool invalidated =
invalid_ && store_invalidation_version == invalidation_version_;
const bool invalidations_enabled = GetInvalidationsEnabled();
RecordPolicyRefreshMetric(scope_, invalidations_enabled, policy_changed,
invalidated);
// If the policy was invalid and the version stored matches the latest
// invalidation version, acknowledge the latest invalidation.
if (invalidated)
AcknowledgeInvalidation();
// Update the highest invalidation version that was handled already.
if (store_invalidation_version > highest_handled_invalidation_version_)
highest_handled_invalidation_version_ = store_invalidation_version;
}
UpdateSubscription(store->policy());
UpdateMaxFetchDelay(store->policy_map());
}
void CloudPolicyInvalidator::OnStoreError(CloudPolicyStore* store) {}
void CloudPolicyInvalidator::HandleInvalidation(
const invalidation::Invalidation& invalidation) {
// Ignore old invalidations.
if (invalid_ && !invalidation.is_unknown_version() &&
invalidation.version() <= invalidation_version_) {
return;
}
if (!invalidation.is_unknown_version() &&
invalidation.version() <= highest_handled_invalidation_version_) {
// If this invalidation version was handled already, acknowledge the
// invalidation but ignore it otherwise.
invalidation.Acknowledge();
return;
}
// If there is still a pending invalidation, acknowledge it, since we only
// care about the latest invalidation.
if (invalid_)
AcknowledgeInvalidation();
// Get the version and payload from the invalidation.
// When an invalidation with unknown version is received, use negative
// numbers based on the number of such invalidations received. This
// ensures that the version numbers do not collide with "real" versions
// (which are positive) or previous invalidations with unknown version.
int64_t version;
std::string payload;
if (invalidation.is_unknown_version()) {
version = -(++unknown_version_invalidation_count_);
} else {
version = invalidation.version();
payload = invalidation.payload();
}
// Ignore the invalidation if it is expired.
const auto last_fetch_time =
base::Time::FromJavaTime(core_->store()->policy()->timestamp());
const auto current_time = clock_->Now();
const bool is_expired =
IsInvalidationExpired(invalidation, last_fetch_time, current_time);
const bool is_missing_payload = payload.empty();
RecordPolicyInvalidationMetric(scope_, is_expired, is_missing_payload);
if (is_expired) {
invalidation.Acknowledge();
return;
}
// Update invalidation state.
invalid_ = true;
invalidation_ = std::make_unique<invalidation::Invalidation>(invalidation);
invalidation_version_ = version;
// In order to prevent the cloud policy server from becoming overwhelmed when
// a policy with many users is modified, delay for a random period of time
// before fetching the policy. Delay for at least 20ms so that if multiple
// invalidations are received in quick succession, only one fetch will be
// performed.
base::TimeDelta delay =
base::TimeDelta::FromMilliseconds(base::RandInt(20, max_fetch_delay_));
// If there is a payload, the policy can be refreshed at any time, so set
// the version and payload on the client immediately. Otherwise, the refresh
// must only run after at least kMissingPayloadDelay minutes.
if (!payload.empty())
core_->client()->SetInvalidationInfo(version, payload);
else
delay += base::TimeDelta::FromMinutes(kMissingPayloadDelay);
// Schedule the policy to be refreshed.
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CloudPolicyInvalidator::RefreshPolicy,
weak_factory_.GetWeakPtr(),
payload.empty() /* is_missing_payload */),
delay);
}
void CloudPolicyInvalidator::UpdateSubscription(
const enterprise_management::PolicyData* policy) {
// Create the Topic based on the policy data.
// If the policy does not specify a Topic, then unregister.
invalidation::Topic topic;
if (!policy || !GetCloudPolicyTopicFromPolicy(*policy, &topic)) {
Unregister();
return;
}
// If the policy topic in the policy data is different from the currently
// registered topic, update the object registration.
if (!is_registered_ || topic != topic_)
Register(topic);
}
void CloudPolicyInvalidator::Register(const invalidation::Topic& topic) {
// Register this handler with the invalidation service if needed.
if (!is_registered_) {
OnInvalidatorStateChange(invalidation_service_->GetInvalidatorState());
invalidation_service_->RegisterInvalidationHandler(this);
}
// Update internal state.
if (invalid_)
AcknowledgeInvalidation();
is_registered_ = true;
topic_ = topic;
UpdateInvalidationsEnabled();
// Update subscription with the invalidation service.
bool success =
invalidation_service_->UpdateInterestedTopics(this, /*topics=*/{topic});
// Do not crash as server might send duplicate invalidation IDs due to
// http://b/119860379.
if (!success) {
LOG(ERROR) << "Failed to subscribe to " << topic
<< " for policy invalidations";
}
base::UmaHistogramBoolean(kMetricPolicyInvalidationRegistration, success);
base::UmaHistogramBoolean(kMetricPolicyInvalidationRegistrationFcm, success);
}
void CloudPolicyInvalidator::Unregister() {
if (is_registered_) {
if (invalid_)
AcknowledgeInvalidation();
CHECK(invalidation_service_->UpdateInterestedTopics(
this, invalidation::TopicSet()));
invalidation_service_->UnregisterInvalidationHandler(this);
is_registered_ = false;
UpdateInvalidationsEnabled();
}
}
void CloudPolicyInvalidator::UpdateMaxFetchDelay(const PolicyMap& policy_map) {
// Try reading the delay from the policy.
const base::Value* delay_policy_value =
policy_map.GetValue(key::kMaxInvalidationFetchDelay);
if (delay_policy_value && delay_policy_value->is_int()) {
set_max_fetch_delay(delay_policy_value->GetInt());
return;
}
set_max_fetch_delay(kMaxFetchDelayDefault);
}
void CloudPolicyInvalidator::set_max_fetch_delay(int delay) {
if (delay < kMaxFetchDelayMin)
max_fetch_delay_ = kMaxFetchDelayMin;
else if (delay > kMaxFetchDelayMax)
max_fetch_delay_ = kMaxFetchDelayMax;
else
max_fetch_delay_ = delay;
}
void CloudPolicyInvalidator::UpdateInvalidationsEnabled() {
bool invalidations_enabled = invalidation_service_enabled_ && is_registered_;
if (invalidations_enabled_ != invalidations_enabled) {
invalidations_enabled_ = invalidations_enabled;
if (invalidations_enabled)
invalidations_enabled_time_ = clock_->Now();
core_->refresh_scheduler()->SetInvalidationServiceAvailability(
invalidations_enabled);
}
}
void CloudPolicyInvalidator::RefreshPolicy(bool is_missing_payload) {
DCHECK(thread_checker_.CalledOnValidThread());
// In the missing payload case, the invalidation version has not been set on
// the client yet, so set it now that the required time has elapsed.
if (is_missing_payload)
core_->client()->SetInvalidationInfo(invalidation_version_, std::string());
core_->refresh_scheduler()->RefreshSoon();
}
void CloudPolicyInvalidator::AcknowledgeInvalidation() {
DCHECK(invalid_);
invalid_ = false;
core_->client()->SetInvalidationInfo(0, std::string());
invalidation_->Acknowledge();
invalidation_.reset();
// Cancel any scheduled policy refreshes.
weak_factory_.InvalidateWeakPtrs();
}
bool CloudPolicyInvalidator::IsPolicyChanged(
const enterprise_management::PolicyData* policy) {
// Determine if the policy changed by comparing its hash value to the
// previous policy's hash value.
uint32_t new_hash_value = 0;
if (policy && policy->has_policy_value())
new_hash_value = base::Hash(policy->policy_value());
bool changed = new_hash_value != policy_hash_value_;
policy_hash_value_ = new_hash_value;
return changed;
}
bool CloudPolicyInvalidator::GetInvalidationsEnabled() {
if (!invalidations_enabled_)
return false;
// If invalidations have been enabled for less than the grace period, then
// consider invalidations to be disabled for metrics reporting.
base::TimeDelta elapsed = clock_->Now() - invalidations_enabled_time_;
return elapsed.InSeconds() >= kInvalidationGracePeriod;
}
} // namespace policy