| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/enterprise/browser/reporting/report_scheduler.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/syslog_logging.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "components/enterprise/browser/controller/browser_dm_token_storage.h" |
| #include "components/enterprise/browser/reporting/chrome_profile_request_generator.h" |
| #include "components/enterprise/browser/reporting/common_pref_names.h" |
| #include "components/enterprise/browser/reporting/real_time_report_controller.h" |
| #include "components/enterprise/browser/reporting/report_generation_config.h" |
| #include "components/enterprise/browser/reporting/report_generator.h" |
| #include "components/enterprise/browser/reporting/reporting_delegate_factory.h" |
| #include "components/policy/core/common/cloud/cloud_policy_client.h" |
| #include "components/policy/core/common/cloud/device_management_service.h" |
| #include "components/policy/core/common/cloud/dm_token.h" |
| #include "components/policy/core/common/policy_logger.h" |
| #include "components/prefs/pref_service.h" |
| |
| namespace em = enterprise_management; |
| |
| namespace enterprise_reporting { |
| |
| namespace { |
| |
| const int kMaximumRetry = 10; // Retry 10 times takes about 15 to 19 hours. |
| |
| bool IsBrowserVersionUploaded(ReportTrigger trigger) { |
| switch (trigger) { |
| case ReportTrigger::kTriggerTimer: |
| case ReportTrigger::kTriggerManual: |
| case ReportTrigger::kTriggerUpdate: |
| case ReportTrigger::kTriggerNewVersion: |
| case ReportTrigger::kTriggerSecurity: |
| return true; |
| case ReportTrigger::kTriggerNone: |
| return false; |
| } |
| } |
| |
| } // namespace |
| |
| ReportScheduler::Delegate::Delegate() = default; |
| ReportScheduler::Delegate::~Delegate() = default; |
| |
| void ReportScheduler::Delegate::OnInitializationCompleted() { |
| if (user_security_signals_service_) { |
| user_security_signals_service_->Start(); |
| } |
| } |
| |
| bool ReportScheduler::Delegate::AreSecurityReportsEnabled() { |
| return user_security_signals_service_ && |
| user_security_signals_service_->IsSecuritySignalsReportingEnabled(); |
| } |
| |
| bool ReportScheduler::Delegate::UseCookiesInUploads() { |
| return user_security_signals_service_ && |
| user_security_signals_service_->ShouldUseCookies(); |
| } |
| |
| void ReportScheduler::Delegate::OnSecuritySignalsUploaded() { |
| if (user_security_signals_service_) { |
| user_security_signals_service_->OnReportUploaded(); |
| } |
| } |
| |
| ReportScheduler::CreateParams::CreateParams() = default; |
| ReportScheduler::CreateParams::CreateParams( |
| ReportScheduler::CreateParams&& other) = default; |
| ReportScheduler::CreateParams& ReportScheduler::CreateParams::operator=( |
| ReportScheduler::CreateParams&& other) = default; |
| ReportScheduler::CreateParams::~CreateParams() = default; |
| |
| void ReportScheduler::Delegate::SetReportTriggerCallback( |
| ReportScheduler::ReportTriggerCallback callback) { |
| DCHECK(trigger_report_callback_.is_null()); |
| trigger_report_callback_ = std::move(callback); |
| } |
| |
| ReportScheduler::ReportScheduler(CreateParams params) |
| : delegate_(std::move(params.delegate)), |
| cloud_policy_client_(params.client), |
| report_generator_(std::move(params.report_generator)), |
| profile_request_generator_(std::move(params.profile_request_generator)), |
| real_time_report_controller_( |
| std::move(params.real_time_report_controller)) { |
| DCHECK(cloud_policy_client_); |
| DCHECK(delegate_); |
| |
| if (report_generator_) { |
| reporting_pref_name_ = std::string(kCloudReportingEnabled); |
| full_report_type_ = ReportType::kFull; |
| } else { |
| reporting_pref_name_ = std::string(kCloudProfileReportingEnabled); |
| full_report_type_ = ReportType::kProfileReport; |
| } |
| |
| delegate_->SetReportTriggerCallback( |
| base::BindRepeating(&ReportScheduler::GenerateAndUploadReport, |
| weak_ptr_factory_.GetWeakPtr())); |
| RegisterPrefObserver(); |
| |
| delegate_->OnInitializationCompleted(); |
| } |
| |
| ReportScheduler::~ReportScheduler() = default; |
| |
| bool ReportScheduler::IsReportingEnabled() const { |
| return delegate_->GetPrefService()->GetBoolean(reporting_pref_name_); |
| } |
| |
| bool ReportScheduler::AreSecurityReportsEnabled() const { |
| return delegate_->AreSecurityReportsEnabled(); |
| } |
| |
| bool ReportScheduler::IsNextReportScheduledForTesting() const { |
| return request_timer_.IsRunning(); |
| } |
| |
| ReportTrigger ReportScheduler::GetActiveTriggerForTesting() const { |
| return active_report_generation_config_.report_trigger; |
| } |
| |
| ReportGenerationConfig ReportScheduler::GetActiveGenerationConfigForTesting() |
| const { |
| return active_report_generation_config_; |
| } |
| |
| void ReportScheduler::QueueReportUploaderForTesting( |
| std::unique_ptr<ReportUploader> uploader) { |
| report_uploaders_for_test_.push_back(std::move(uploader)); |
| } |
| |
| ReportScheduler::Delegate* ReportScheduler::GetDelegateForTesting() { |
| return delegate_.get(); |
| } |
| |
| void ReportScheduler::OnDMTokenUpdated() { |
| OnReportEnabledPrefChanged(); |
| if (real_time_report_controller_) { |
| real_time_report_controller_->OnDMTokenUpdated(GetDMToken()); |
| } |
| } |
| |
| void ReportScheduler::UploadFullReport(base::OnceClosure on_report_uploaded) { |
| ReportTrigger trigger = kTriggerNone; |
| if (IsReportingEnabled()) { |
| trigger = kTriggerManual; |
| } else if (delegate_->AreSecurityReportsEnabled()) { |
| trigger = kTriggerSecurity; |
| } else { |
| VLOG(1) << "Reporting is not enabled."; |
| std::move(on_report_uploaded).Run(); |
| return; |
| } |
| |
| if (on_manual_report_uploaded_) { |
| VLOG(1) << "Another report is uploading."; |
| std::move(on_report_uploaded).Run(); |
| return; |
| } |
| on_manual_report_uploaded_ = std::move(on_report_uploaded); |
| GenerateAndUploadReport(trigger); |
| } |
| |
| void ReportScheduler::RegisterPrefObserver() { |
| pref_change_registrar_.Init(delegate_->GetPrefService()); |
| pref_change_registrar_.Add( |
| reporting_pref_name_, |
| base::BindRepeating(&ReportScheduler::OnReportEnabledPrefChanged, |
| base::Unretained(this))); |
| |
| // Trigger first pref check during launch process. |
| OnDMTokenUpdated(); |
| } |
| |
| void ReportScheduler::OnReportEnabledPrefChanged() { |
| if (!IsReportingEnabled()) { |
| Stop(); |
| return; |
| } |
| |
| // For Chrome OS, it needn't register the cloud policy client here. The |
| // |dm_token| and |client_id| should have already existed after the client is |
| // initialized, and will keep valid during whole life-cycle. |
| #if !BUILDFLAG(IS_CHROMEOS) |
| if (!SetupBrowserPolicyClientRegistration()) { |
| Stop(); |
| return; |
| } |
| #endif |
| |
| // Start the periodic report timer. |
| RestartReportTimer(); |
| if (!pref_change_registrar_.IsObserved(kCloudReportingUploadFrequency)) { |
| pref_change_registrar_.Add( |
| kCloudReportingUploadFrequency, |
| base::BindRepeating(&ReportScheduler::RestartReportTimer, |
| base::Unretained(this))); |
| } |
| |
| // Only device report generator support real time partial version report with |
| // DM Server. With longer term, this should use `real_time_report_controller_` |
| // instead. |
| if (report_generator_) { |
| delegate_->StartWatchingUpdatesIfNeeded( |
| delegate_->GetPrefService()->GetTime(kLastUploadTimestamp), |
| delegate_->GetPrefService()->GetTimeDelta( |
| kCloudReportingUploadFrequency)); |
| } |
| } |
| |
| void ReportScheduler::Stop() { |
| request_timer_.Stop(); |
| if (report_generator_) |
| delegate_->StopWatchingUpdates(); |
| report_uploader_.reset(); |
| if (pref_change_registrar_.IsObserved(kCloudReportingUploadFrequency)) |
| pref_change_registrar_.Remove(kCloudReportingUploadFrequency); |
| } |
| |
| void ReportScheduler::RestartReportTimer() { |
| request_timer_.Stop(); |
| Start(delegate_->GetPrefService()->GetTime(kLastUploadTimestamp)); |
| } |
| |
| bool ReportScheduler::SetupBrowserPolicyClientRegistration() { |
| if (cloud_policy_client_->is_registered()) |
| return true; |
| |
| auto dm_token = GetDMToken(); |
| std::string client_id; |
| if (profile_request_generator_) { |
| // Get token for profile reporting |
| client_id = delegate_->GetProfileClientId(); |
| } else { |
| // Get token for browser reporting |
| #if !BUILDFLAG(IS_CHROMEOS) |
| client_id = policy::BrowserDMTokenStorage::Get()->RetrieveClientId(); |
| #else |
| NOTREACHED(); |
| #endif |
| } |
| if (!dm_token.is_valid() || client_id.empty()) { |
| VLOG(1) |
| << "Enterprise reporting is disabled because device or profile is not " |
| "enrolled."; |
| return false; |
| } |
| |
| cloud_policy_client_->SetupRegistration( |
| dm_token.value(), client_id, |
| /*user_affiliation_ids=*/std::vector<std::string>()); |
| return true; |
| } |
| |
| void ReportScheduler::Start(base::Time last_upload_time) { |
| // The next report is triggered 24h after the previous was uploaded. |
| const base::Time next_upload_time = |
| last_upload_time + |
| delegate_->GetPrefService()->GetTimeDelta(kCloudReportingUploadFrequency); |
| if (VLOG_IS_ON(1)) { |
| base::TimeDelta first_request_delay = next_upload_time - base::Time::Now(); |
| VLOG(1) << "Schedule the first report in about " |
| << first_request_delay.InHours() << " hour(s) and " |
| << first_request_delay.InMinutes() % 60 << " minute(s)."; |
| } |
| request_timer_.Start(FROM_HERE, next_upload_time, |
| base::BindOnce(&ReportScheduler::GenerateAndUploadReport, |
| base::Unretained(this), kTriggerTimer)); |
| } |
| |
| void ReportScheduler::GenerateAndUploadReport(ReportTrigger trigger) { |
| if (delegate_->AreSecurityReportsEnabled()) { |
| // Does nothing if client is already registered. |
| SetupBrowserPolicyClientRegistration(); |
| } |
| |
| if (active_report_generation_config_.report_trigger != kTriggerNone) { |
| // A report is already being generated. Remember this trigger to be handled |
| // once the current report completes. |
| pending_triggers_ |= trigger; |
| return; |
| } |
| |
| ReportType report_type = TriggerToReportType(trigger); |
| SecuritySignalsMode signals_mode = SecuritySignalsMode::kNoSignals; |
| if (report_type == ReportType::kProfileReport) { |
| signals_mode = delegate_->AreSecurityReportsEnabled() |
| ? (trigger == ReportTrigger::kTriggerSecurity |
| ? SecuritySignalsMode::kSignalsOnly |
| : SecuritySignalsMode::kSignalsAttached) |
| : SecuritySignalsMode::kNoSignals; |
| } |
| |
| active_report_generation_config_ = ReportGenerationConfig( |
| trigger, report_type, signals_mode, delegate_->UseCookiesInUploads()); |
| |
| VLOG_POLICY(1, REPORTING) |
| << "Starting report generation with the following configuration: " |
| << active_report_generation_config_.ToString(); |
| |
| if (report_type == ReportType::kProfileReport) { |
| DCHECK(profile_request_generator_); |
| profile_request_generator_->Generate( |
| active_report_generation_config_, |
| base::BindOnce(&ReportScheduler::OnReportGenerated, |
| base::Unretained(this))); |
| } else { |
| DCHECK(report_generator_); |
| report_generator_->Generate( |
| active_report_generation_config_.report_type, |
| base::BindOnce(&ReportScheduler::OnReportGenerated, |
| base::Unretained(this))); |
| } |
| } |
| |
| void ReportScheduler::OnReportGenerated(ReportRequestQueue requests) { |
| DCHECK_NE(active_report_generation_config_.report_trigger, |
| ReportTrigger::kTriggerNone); |
| if (requests.empty()) { |
| SYSLOG(ERROR) |
| << "No cloud report can be generated. Likely the report is too large."; |
| // Do not restart the periodic report timer, as it's likely that subsequent |
| // attempts to generate full reports would also fail. |
| active_report_generation_config_ = |
| ReportGenerationConfig(ReportTrigger::kTriggerNone); |
| RunPendingTriggers(); |
| return; |
| } |
| VLOG(1) << "Uploading enterprise report."; |
| if (!report_uploader_ && report_uploaders_for_test_.size() > 0) { |
| report_uploader_ = std::move(report_uploaders_for_test_.front()); |
| report_uploaders_for_test_.erase(report_uploaders_for_test_.begin()); |
| } else if (!report_uploader_) { |
| report_uploader_ = |
| std::make_unique<ReportUploader>(cloud_policy_client_, kMaximumRetry); |
| } |
| |
| RecordUploadTrigger(); |
| if (active_report_generation_config_.security_signals_mode != |
| SecuritySignalsMode::kNoSignals) { |
| delegate_->GetPrefService()->SetTime(kLastSignalsUploadAttemptTimestamp, |
| base::Time::Now()); |
| } |
| |
| report_uploader_->SetRequestAndUpload( |
| active_report_generation_config_, std::move(requests), |
| base::BindOnce(&ReportScheduler::OnReportUploaded, |
| base::Unretained(this))); |
| } |
| |
| void ReportScheduler::OnReportUploaded(ReportUploader::ReportStatus status) { |
| DCHECK_NE(active_report_generation_config_.report_trigger, |
| ReportTrigger::kTriggerNone); |
| VLOG(1) << "The enterprise report upload result " << status << "."; |
| switch (status) { |
| case ReportUploader::kSuccess: |
| // Schedule the next report for success. Reset uploader to reset failure |
| // count. |
| report_uploader_.reset(); |
| if (IsBrowserVersionUploaded( |
| active_report_generation_config_.report_trigger)) { |
| delegate_->OnBrowserVersionUploaded(); |
| } |
| |
| // Signals-only report does not contain most content of a status report |
| // and should not update this timestamp. |
| if (active_report_generation_config_.report_trigger != |
| ReportTrigger::kTriggerSecurity) { |
| delegate_->GetPrefService()->SetTime(kLastUploadSucceededTimestamp, |
| base::Time::Now()); |
| } |
| |
| if (active_report_generation_config_.security_signals_mode != |
| SecuritySignalsMode::kNoSignals) { |
| delegate_->GetPrefService()->SetTime( |
| kLastSignalsUploadSucceededTimestamp, base::Time::Now()); |
| delegate_->GetPrefService()->SetString( |
| kLastSignalsUploadSucceededConfig, |
| active_report_generation_config_.ToString()); |
| } |
| [[fallthrough]]; |
| case ReportUploader::kTransientError: |
| // Stop retrying and schedule the next report to avoid stale report. |
| // Failure count is not reset so retry delay remains. |
| if (active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerTimer || |
| active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerManual) { |
| const base::Time now = base::Time::Now(); |
| delegate_->GetPrefService()->SetTime(kLastUploadTimestamp, now); |
| if (IsReportingEnabled()) |
| Start(now); |
| } |
| break; |
| case ReportUploader::kPersistentError: |
| Stop(); |
| // No future upload until Chrome relaunch or pref change event. |
| break; |
| } |
| |
| if ((active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerManual || |
| active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerTimer)) { |
| // Timer and Manual report are exactly same. If we just uploaded one, skip |
| // the other. |
| if (pending_triggers_ & ReportTrigger::kTriggerTimer) { |
| pending_triggers_ -= ReportTrigger::kTriggerTimer; |
| } |
| if (pending_triggers_ & ReportTrigger::kTriggerManual) { |
| pending_triggers_ -= ReportTrigger::kTriggerManual; |
| } |
| } |
| |
| if (active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerManual || |
| active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerTimer || |
| active_report_generation_config_.report_trigger == |
| ReportTrigger::kTriggerSecurity) { |
| if (on_manual_report_uploaded_) { |
| std::move(on_manual_report_uploaded_).Run(); |
| } |
| |
| if (active_report_generation_config_.security_signals_mode != |
| SecuritySignalsMode::kNoSignals) { |
| delegate_->OnSecuritySignalsUploaded(); |
| |
| // A full report includes security signals already, we don't need another |
| // security signals only report until the timer runs out again. |
| if (pending_triggers_ & ReportTrigger::kTriggerSecurity) { |
| pending_triggers_ -= ReportTrigger::kTriggerSecurity; |
| } |
| } |
| } |
| |
| active_report_generation_config_ = |
| ReportGenerationConfig(ReportTrigger::kTriggerNone); |
| RunPendingTriggers(); |
| } |
| |
| void ReportScheduler::RunPendingTriggers() { |
| DCHECK_EQ(active_report_generation_config_.report_trigger, |
| ReportTrigger::kTriggerNone); |
| if (!pending_triggers_) |
| return; |
| |
| // Timer-triggered reports are a superset of those triggered by an update or a |
| // new version, so favor them and consider that they serve all purposes. |
| |
| ReportTrigger trigger; |
| if ((pending_triggers_ & ReportTrigger::kTriggerTimer) != 0) { |
| // Timer-triggered reports contain data of all other report types. |
| trigger = ReportTrigger::kTriggerTimer; |
| pending_triggers_ = 0; |
| } else if ((pending_triggers_ & ReportTrigger::kTriggerManual) != 0) { |
| // Manual-triggered reports also contains all data. |
| trigger = kTriggerManual; |
| pending_triggers_ = 0; |
| } else if ((pending_triggers_ & ReportTrigger::kTriggerSecurity) != 0) { |
| trigger = kTriggerSecurity; |
| pending_triggers_ -= ReportTrigger::kTriggerSecurity; |
| } else { |
| // Update and NewVersion triggers lead to the same report content being |
| // uploaded. |
| if ((pending_triggers_ & ReportTrigger::kTriggerUpdate) != 0) { |
| trigger = ReportTrigger::kTriggerUpdate; |
| pending_triggers_ -= ReportTrigger::kTriggerUpdate; |
| } |
| |
| if ((pending_triggers_ & ReportTrigger::kTriggerNewVersion) != 0) { |
| trigger = ReportTrigger::kTriggerNewVersion; |
| pending_triggers_ -= ReportTrigger::kTriggerNewVersion; |
| } |
| } |
| |
| GenerateAndUploadReport(trigger); |
| } |
| |
| void ReportScheduler::RecordUploadTrigger() { |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class Sample { |
| kNone = 0, |
| kTimer = 1, |
| kUpdate = 2, |
| kNewVersion = 3, |
| kExtensionRequest = 4, // Deprecated. |
| kExtensionRequestRealTime = 5, // Deprecated. |
| kManual = 6, |
| kSecurity = 7, |
| kMaxValue = kSecurity |
| } sample = Sample::kNone; |
| switch (active_report_generation_config_.report_trigger) { |
| case ReportTrigger::kTriggerNone: |
| break; |
| case ReportTrigger::kTriggerTimer: |
| sample = Sample::kTimer; |
| break; |
| case ReportTrigger::kTriggerManual: |
| sample = Sample::kManual; |
| break; |
| case ReportTrigger::kTriggerUpdate: |
| sample = Sample::kUpdate; |
| break; |
| case ReportTrigger::kTriggerNewVersion: |
| sample = Sample::kNewVersion; |
| break; |
| case ReportTrigger::kTriggerSecurity: |
| sample = Sample::kSecurity; |
| break; |
| } |
| base::UmaHistogramEnumeration("Enterprise.CloudReportingUploadTrigger", |
| sample); |
| |
| if (active_report_generation_config_.security_signals_mode != |
| SecuritySignalsMode::kNoSignals) { |
| base::UmaHistogramEnumeration( |
| "Enterprise.SecurityReport.User.Mode", |
| active_report_generation_config_.security_signals_mode); |
| } |
| } |
| |
| ReportType ReportScheduler::TriggerToReportType(ReportTrigger trigger) { |
| switch (trigger) { |
| case ReportTrigger::kTriggerNone: |
| NOTREACHED(); |
| case ReportTrigger::kTriggerTimer: |
| case ReportTrigger::kTriggerManual: |
| return full_report_type_; |
| case ReportTrigger::kTriggerUpdate: |
| return ReportType::kBrowserVersion; |
| case ReportTrigger::kTriggerNewVersion: |
| return ReportType::kBrowserVersion; |
| case ReportTrigger::kTriggerSecurity: |
| // Security triggers are not supported at the browser-level yet. |
| return ReportType::kProfileReport; |
| } |
| } |
| |
| policy::DMToken ReportScheduler::GetDMToken() { |
| #if BUILDFLAG(IS_CHROMEOS) |
| return policy::DMToken::CreateValidToken(cloud_policy_client_->dm_token()); |
| #else |
| if (profile_request_generator_) { |
| return delegate_->GetProfileDMToken(); |
| } else { |
| return policy::BrowserDMTokenStorage::Get()->RetrieveDMToken(); |
| } |
| #endif |
| } |
| |
| } // namespace enterprise_reporting |