| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/webui/certificate_provisioning_ui_handler.h" |
| |
| #include <string> |
| |
| #include "base/check_is_test.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/cert_provisioning/cert_provisioning_common.h" |
| #include "chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.h" |
| #include "chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler_user_service.h" |
| #include "chrome/browser/ash/cert_provisioning/cert_provisioning_worker.h" |
| #include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/browser_process_platform_part.h" |
| #include "chrome/common/net/x509_certificate_model.h" |
| #include "chrome/common/net/x509_certificate_model_nss.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/policy/core/browser/cloud/message_util.h" |
| #include "components/policy/core/common/cloud/cloud_policy_constants.h" |
| #include "components/user_manager/user.h" |
| #include "content/public/browser/web_ui.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/time_format.h" |
| |
| static_assert( |
| BUILDFLAG(IS_CHROMEOS), |
| "CertificateProvisioningUIHandler is available only for ChromeOS"); |
| |
| using ash::cert_provisioning::BackendServerError; |
| using ash::cert_provisioning::CertProvisioningScheduler; |
| using ash::cert_provisioning::CertProvisioningSchedulerUserServiceFactory; |
| using ash::cert_provisioning::CertProvisioningWorkerState; |
| |
| namespace chromeos::cert_provisioning { |
| |
| namespace { |
| |
| CertProvisioningScheduler* GetUserScheduler(Profile* profile) { |
| if (!ash::ProfileHelper::IsPrimaryProfile(profile)) { |
| return nullptr; |
| } |
| |
| auto* user_service = |
| CertProvisioningSchedulerUserServiceFactory::GetForProfile(profile); |
| if (!user_service) { |
| return nullptr; |
| } |
| |
| return user_service->scheduler(); |
| } |
| |
| CertProvisioningScheduler* GetDeviceScheduler(Profile* profile) { |
| if (!ash::ProfileHelper::IsPrimaryProfile(profile)) { |
| return nullptr; |
| } |
| |
| const user_manager::User* user = |
| ash::ProfileHelper::Get()->GetUserByProfile(profile); |
| if (!user || !user->IsAffiliated()) { |
| return nullptr; |
| } |
| |
| policy::BrowserPolicyConnectorAsh* connector = |
| g_browser_process->platform_part()->browser_policy_connector_ash(); |
| return connector->GetDeviceCertProvisioningScheduler(); |
| } |
| |
| // Returns localized representation for the state of a certificate provisioning |
| // process. |
| std::u16string StateToText(CertProvisioningWorkerState state) { |
| switch (state) { |
| case CertProvisioningWorkerState ::kInitState: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR); |
| case CertProvisioningWorkerState ::kKeypairGenerated: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING); |
| case CertProvisioningWorkerState::kStartCsrResponseReceived: |
| // Intentional fall-through. |
| case CertProvisioningWorkerState::kVaChallengeFinished: |
| // Intentional fall-through. |
| case CertProvisioningWorkerState::kKeyRegistered: |
| // Intentional fall-through. |
| case CertProvisioningWorkerState::kKeypairMarked: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR); |
| case CertProvisioningWorkerState::kSignCsrFinished: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING); |
| case CertProvisioningWorkerState::kFinishCsrResponseReceived: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_WAITING_FOR_CA); |
| case CertProvisioningWorkerState::kSucceeded: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_SUCCESS); |
| case CertProvisioningWorkerState::kFailed: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_FAILURE); |
| case CertProvisioningWorkerState::kInconsistentDataError: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING); |
| case CertProvisioningWorkerState::kCanceled: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_CANCELED); |
| case CertProvisioningWorkerState::kReadyForNextOperation: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_READY_FOR_NEXT_OPERATION); |
| case CertProvisioningWorkerState::kAuthorizeInstructionReceived: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_AUTHORIZE_INSTRUCTION_RECEIVED); |
| case CertProvisioningWorkerState::kProofOfPossessionInstructionReceived: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PROOF_OF_POSSESSION_INSTRUCTION_RECEIVED); |
| case CertProvisioningWorkerState::kImportCertificateInstructionReceived: |
| return l10n_util::GetStringUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_IMPORT_CERTIFICATE_INSTRUCTION_RECEIVED); |
| } |
| NOTREACHED(); |
| } |
| |
| // Returns the status message of the process. |
| // The status message is expanded by the failure message if the process failed |
| // and the error message is non-empty. |
| std::u16string MakeStatusMessage( |
| bool did_fail, |
| CertProvisioningWorkerState state, |
| const std::optional<std::string>& failure_message) { |
| if (!did_fail) { |
| return StateToText(state); |
| } |
| std::u16string status_message = |
| StateToText(CertProvisioningWorkerState::kFailed); |
| if (failure_message.has_value()) { |
| status_message += base::UTF8ToUTF16(": " + failure_message.value()); |
| } |
| return status_message; |
| } |
| |
| // Returns a localized representation of the last update time as a delay (e.g. |
| // "5 minutes ago". |
| std::u16string GetTimeSinceLastUpdate(base::Time last_update_time) { |
| const base::Time now = base::Time::NowFromSystemTime(); |
| if (last_update_time.is_null() || last_update_time > now) { |
| return std::u16string(); |
| } |
| const base::TimeDelta elapsed_time = now - last_update_time; |
| return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED, |
| ui::TimeFormat::LENGTH_SHORT, elapsed_time); |
| } |
| |
| std::u16string GetMessageFromBackendError(const BackendServerError& error) { |
| std::u16string time_u16 = base::UTF8ToUTF16(base::TimeFormatHTTP(error.time)); |
| // FormatDeviceManagementStatus will return "Unknown error" if the value after |
| // cast is not actually an existing enum value. |
| return l10n_util::GetStringFUTF16( |
| IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_DMSERVER_ERROR_MESSAGE, |
| policy::FormatDeviceManagementStatus(error.status), time_u16); |
| } |
| |
| void AppendWorkerStatus(CertProvisioningScheduler* scheduler, |
| bool is_device_wide, |
| base::Value::List& all_processes) { |
| if (!scheduler) { |
| return; |
| } |
| |
| const auto& worker_map = scheduler->GetWorkers(); |
| for (const auto& [profile_id, worker] : worker_map) { |
| base::Value::Dict entry; |
| entry.Set("processId", worker->GetProcessId()); |
| entry.Set("certProfileId", profile_id); |
| entry.Set("certProfileName", worker->GetCertProfile().name); |
| entry.Set("isDeviceWide", is_device_wide); |
| entry.Set("timeSinceLastUpdate", |
| GetTimeSinceLastUpdate(worker->GetLastUpdateTime())); |
| const auto& backend_error = worker->GetLastBackendServerError(); |
| entry.Set("lastUnsuccessfulMessage", |
| backend_error.has_value() |
| ? GetMessageFromBackendError(*backend_error) |
| : std::u16string()); |
| entry.Set("stateId", static_cast<int>(worker->GetState())); |
| entry.Set("status", MakeStatusMessage(/*did_fail=*/false, |
| worker->GetState(), std::nullopt)); |
| entry.Set("publicKey", |
| x509_certificate_model::ProcessRawSubjectPublicKeyInfo( |
| worker->GetPublicKey())); |
| |
| all_processes.Append(std::move(entry)); |
| } |
| |
| const auto& failed_workers_map = scheduler->GetFailedCertProfileIds(); |
| for (const auto& [profile_id, worker] : failed_workers_map) { |
| base::Value::Dict entry; |
| entry.Set("processId", worker.process_id); |
| entry.Set("certProfileId", profile_id); |
| entry.Set("certProfileName", worker.cert_profile_name); |
| entry.Set("isDeviceWide", is_device_wide); |
| entry.Set("timeSinceLastUpdate", |
| GetTimeSinceLastUpdate(worker.last_update_time)); |
| entry.Set("lastUnsuccessfulMessage", std::u16string()); |
| entry.Set("stateId", static_cast<int>(worker.state_before_failure)); |
| entry.Set("status", |
| MakeStatusMessage(/*did_fail=*/true, worker.state_before_failure, |
| worker.failure_message)); |
| entry.Set("publicKey", |
| x509_certificate_model::ProcessRawSubjectPublicKeyInfo( |
| worker.public_key)); |
| |
| all_processes.Append(std::move(entry)); |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<CertificateProvisioningUiHandler> |
| CertificateProvisioningUiHandler::CreateForProfile(Profile* user_profile) { |
| return std::make_unique<CertificateProvisioningUiHandler>( |
| GetUserScheduler(user_profile), GetDeviceScheduler(user_profile)); |
| } |
| |
| CertificateProvisioningUiHandler::CertificateProvisioningUiHandler( |
| CertProvisioningScheduler* user_scheduler, |
| CertProvisioningScheduler* device_scheduler) |
| : user_scheduler_(user_scheduler), device_scheduler_(device_scheduler) { |
| if (user_scheduler_) { |
| user_subscription_ = user_scheduler_->AddObserver( |
| base::BindRepeating(&CertificateProvisioningUiHandler::OnStateChanged, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| if (device_scheduler_) { |
| device_subscription_ = device_scheduler_->AddObserver( |
| base::BindRepeating(&CertificateProvisioningUiHandler::OnStateChanged, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| CertificateProvisioningUiHandler::~CertificateProvisioningUiHandler() = default; |
| |
| void CertificateProvisioningUiHandler::RegisterMessages() { |
| // Passing base::Unretained(this) to |
| // web_ui()->RegisterMessageCallback is fine because in chrome Web |
| // UI, web_ui() has acquired ownership of |this| and maintains the life time |
| // of |this| accordingly. |
| web_ui()->RegisterMessageCallback( |
| "refreshCertificateProvisioningProcessses", |
| base::BindRepeating(&CertificateProvisioningUiHandler:: |
| HandleRefreshCertificateProvisioningProcesses, |
| base::Unretained(this))); |
| web_ui()->RegisterMessageCallback( |
| "triggerCertificateProvisioningProcessUpdate", |
| base::BindRepeating(&CertificateProvisioningUiHandler:: |
| HandleTriggerCertificateProvisioningProcessUpdate, |
| base::Unretained(this))); |
| web_ui()->RegisterMessageCallback( |
| "triggerCertificateProvisioningProcessReset", |
| base::BindRepeating(&CertificateProvisioningUiHandler:: |
| HandleTriggerCertificateProvisioningProcessReset, |
| base::Unretained(this))); |
| } |
| |
| void CertificateProvisioningUiHandler::OnStateChanged() { |
| // If Javascript is not allowed yet, the UI will request a refresh during its |
| // first message to the handler. |
| if (!IsJavascriptAllowed()) { |
| return; |
| } |
| |
| RefreshCertificateProvisioningProcesses(); |
| } |
| |
| unsigned int |
| CertificateProvisioningUiHandler::ReadAndResetUiRefreshCountForTesting() { |
| unsigned int value = ui_refresh_count_for_testing_; |
| ui_refresh_count_for_testing_ = 0; |
| return value; |
| } |
| |
| void CertificateProvisioningUiHandler:: |
| HandleRefreshCertificateProvisioningProcesses( |
| const base::Value::List& args) { |
| CHECK_EQ(0U, args.size()); |
| AllowJavascript(); |
| RefreshCertificateProvisioningProcesses(); |
| } |
| |
| void CertificateProvisioningUiHandler:: |
| HandleTriggerCertificateProvisioningProcessUpdate( |
| const base::Value::List& args) { |
| CHECK_EQ(1U, args.size()); |
| const base::Value& cert_profile_id = args[0]; |
| if (!cert_profile_id.is_string()) { |
| return; |
| } |
| |
| if (user_scheduler_ && |
| user_scheduler_->UpdateOneWorker(cert_profile_id.GetString())) { |
| return; |
| } |
| |
| if (device_scheduler_ && |
| device_scheduler_->UpdateOneWorker(cert_profile_id.GetString())) { |
| return; |
| } |
| |
| if (user_scheduler_ || device_scheduler_) { |
| LOG(ERROR) << "Updating cert_profile_id was not found. id:" |
| << cert_profile_id.GetString() |
| << " user_scheduler:" << bool(user_scheduler_) |
| << " device_scheduler:" << bool(device_scheduler_); |
| } |
| } |
| |
| void CertificateProvisioningUiHandler:: |
| HandleTriggerCertificateProvisioningProcessReset( |
| const base::Value::List& args) { |
| CHECK_EQ(1U, args.size()); |
| const base::Value& cert_profile_id = args[0]; |
| if (!cert_profile_id.is_string()) { |
| return; |
| } |
| |
| if (user_scheduler_ && |
| user_scheduler_->ResetOneWorker(cert_profile_id.GetString())) { |
| return; |
| } |
| if (device_scheduler_ && |
| device_scheduler_->ResetOneWorker(cert_profile_id.GetString())) { |
| return; |
| } |
| |
| if (user_scheduler_ || device_scheduler_) { |
| LOG(ERROR) << "Resetting cert_profile_id was not found. id:" |
| << cert_profile_id.GetString() |
| << " user_scheduler:" << bool(user_scheduler_) |
| << " device_scheduler:" << bool(device_scheduler_); |
| } |
| } |
| |
| void CertificateProvisioningUiHandler:: |
| RefreshCertificateProvisioningProcesses() { |
| base::Value::List all_processes; |
| AppendWorkerStatus(user_scheduler_, /*is_device_wide=*/false, all_processes); |
| AppendWorkerStatus(device_scheduler_, /*is_device_wide=*/true, all_processes); |
| |
| ++ui_refresh_count_for_testing_; |
| FireWebUIListener("certificate-provisioning-processes-changed", |
| all_processes); |
| } |
| |
| } // namespace chromeos::cert_provisioning |