| // 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 "base/bind.h" |
| #include "base/hash.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram.h" |
| #include "base/rand_util.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/invalidation/invalidation_service.h" |
| #include "components/invalidation/object_id_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 "policy/policy_constants.h" |
| |
| namespace policy { |
| |
| 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; |
| const int CloudPolicyInvalidator::kUnknownVersionIgnorePeriod = 30; |
| const int CloudPolicyInvalidator::kMaxInvalidationTimeDelta = 300; |
| |
| CloudPolicyInvalidator::CloudPolicyInvalidator( |
| CloudPolicyCore* core, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner, |
| scoped_ptr<base::Clock> clock) |
| : state_(UNINITIALIZED), |
| core_(core), |
| task_runner_(task_runner), |
| clock_(clock.Pass()), |
| invalidation_service_(NULL), |
| invalidations_enabled_(false), |
| invalidation_service_enabled_(false), |
| is_registered_(false), |
| invalid_(false), |
| invalidation_version_(0), |
| unknown_version_invalidation_count_(0), |
| weak_factory_(this), |
| max_fetch_delay_(kMaxFetchDelayDefault), |
| policy_hash_value_(0) { |
| DCHECK(core); |
| DCHECK(task_runner.get()); |
| } |
| |
| 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( |
| syncer::InvalidatorState state) { |
| DCHECK(state_ == STARTED); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| invalidation_service_enabled_ = state == syncer::INVALIDATIONS_ENABLED; |
| UpdateInvalidationsEnabled(); |
| } |
| |
| void CloudPolicyInvalidator::OnIncomingInvalidation( |
| const syncer::ObjectIdInvalidationMap& invalidation_map) { |
| DCHECK(state_ == STARTED); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| const syncer::SingleObjectInvalidationSet& list = |
| invalidation_map.ForObject(object_id_); |
| if (list.IsEmpty()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| // Acknowledge all except the invalidation with the highest version. |
| syncer::SingleObjectInvalidationSet::const_reverse_iterator 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 "Cloud"; } |
| |
| 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_) { |
| // Update the kMetricPolicyRefresh histogram. |
| UMA_HISTOGRAM_ENUMERATION( |
| kMetricPolicyRefresh, |
| GetPolicyRefreshMetric(policy_changed), |
| METRIC_POLICY_REFRESH_SIZE); |
| |
| // If the policy was invalid and the version stored matches the latest |
| // invalidation version, acknowledge the latest invalidation. |
| if (invalid_ && store->invalidation_version() == invalidation_version_) |
| AcknowledgeInvalidation(); |
| } |
| |
| UpdateRegistration(store->policy()); |
| UpdateMaxFetchDelay(store->policy_map()); |
| } |
| |
| void CloudPolicyInvalidator::OnStoreError(CloudPolicyStore* store) {} |
| |
| void CloudPolicyInvalidator::HandleInvalidation( |
| const syncer::Invalidation& invalidation) { |
| // Ignore old invalidations. |
| if (invalid_ && |
| !invalidation.is_unknown_version() && |
| invalidation.version() <= invalidation_version_) { |
| 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 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. |
| bool is_expired = IsInvalidationExpired(version); |
| UMA_HISTOGRAM_ENUMERATION( |
| kMetricPolicyInvalidations, |
| GetInvalidationMetric(payload.empty(), is_expired), |
| POLICY_INVALIDATION_TYPE_SIZE); |
| if (is_expired) { |
| invalidation.Acknowledge(); |
| return; |
| } |
| |
| // Update invalidation state. |
| invalid_ = true; |
| invalidation_.reset(new syncer::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::Bind( |
| &CloudPolicyInvalidator::RefreshPolicy, |
| weak_factory_.GetWeakPtr(), |
| payload.empty() /* is_missing_payload */), |
| delay); |
| } |
| |
| void CloudPolicyInvalidator::UpdateRegistration( |
| const enterprise_management::PolicyData* policy) { |
| // Create the ObjectId based on the policy data. |
| // If the policy does not specify an the ObjectId, then unregister. |
| if (!policy || |
| !policy->has_invalidation_source() || |
| !policy->has_invalidation_name()) { |
| Unregister(); |
| return; |
| } |
| invalidation::ObjectId object_id( |
| policy->invalidation_source(), |
| policy->invalidation_name()); |
| |
| // If the policy object id in the policy data is different from the currently |
| // registered object id, update the object registration. |
| if (!is_registered_ || !(object_id == object_id_)) |
| Register(object_id); |
| } |
| |
| void CloudPolicyInvalidator::Register(const invalidation::ObjectId& object_id) { |
| // 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; |
| object_id_ = object_id; |
| UpdateInvalidationsEnabled(); |
| |
| // Update registration with the invalidation service. |
| syncer::ObjectIdSet ids; |
| ids.insert(object_id); |
| invalidation_service_->UpdateRegisteredInvalidationIds(this, ids); |
| } |
| |
| void CloudPolicyInvalidator::Unregister() { |
| if (is_registered_) { |
| if (invalid_) |
| AcknowledgeInvalidation(); |
| invalidation_service_->UpdateRegisteredInvalidationIds( |
| this, |
| syncer::ObjectIdSet()); |
| invalidation_service_->UnregisterInvalidationHandler(this); |
| is_registered_ = false; |
| UpdateInvalidationsEnabled(); |
| } |
| } |
| |
| void CloudPolicyInvalidator::UpdateMaxFetchDelay(const PolicyMap& policy_map) { |
| int delay; |
| |
| // 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->GetAsInteger(&delay)) { |
| set_max_fetch_delay(delay); |
| 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 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::IsInvalidationExpired(int64 version) { |
| base::Time last_fetch_time = base::Time::UnixEpoch() + |
| base::TimeDelta::FromMilliseconds(core_->store()->policy()->timestamp()); |
| |
| // If the version is unknown, consider the invalidation invalid if the |
| // policy was fetched very recently. |
| if (version < 0) { |
| base::TimeDelta elapsed = clock_->Now() - last_fetch_time; |
| return elapsed.InSeconds() < kUnknownVersionIgnorePeriod; |
| } |
| |
| // The invalidation version is the timestamp in microseconds. If the |
| // invalidation occurred before the last policy fetch, then the invalidation |
| // is expired. Time is added to the invalidation to err on the side of not |
| // expired. |
| base::Time invalidation_time = base::Time::UnixEpoch() + |
| base::TimeDelta::FromMicroseconds(version) + |
| base::TimeDelta::FromSeconds(kMaxInvalidationTimeDelta); |
| return invalidation_time < last_fetch_time; |
| } |
| |
| int CloudPolicyInvalidator::GetPolicyRefreshMetric(bool policy_changed) { |
| if (policy_changed) { |
| if (invalid_) |
| return METRIC_POLICY_REFRESH_INVALIDATED_CHANGED; |
| if (GetInvalidationsEnabled()) |
| return METRIC_POLICY_REFRESH_CHANGED; |
| return METRIC_POLICY_REFRESH_CHANGED_NO_INVALIDATIONS; |
| } |
| if (invalid_) |
| return METRIC_POLICY_REFRESH_INVALIDATED_UNCHANGED; |
| return METRIC_POLICY_REFRESH_UNCHANGED; |
| } |
| |
| int CloudPolicyInvalidator::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; |
| } |
| |
| 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 |