chromium/ui/webui/resources/cr_components/certificate_manager/certificate_manager_v2.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview The 'certificate-manager-v2' component is a newer way for
 * showing and managing TLS certificates. This is tied to the Chrome Root Store
 * and Chrome Cert Management Enterprise policies launch.
 */

import './certificate_list_v2.js';
import './certificate_info_dialog.js';
import './certificate_password_dialog.js';
import './certificate_subpage_v2.js';
import './certificate_manager_v2_icons.html.js';
import './certificate_manager_style_v2.css.js';
import './crs_section_v2.js';
import '//resources/cr_elements/cr_icon/cr_icon.js';
import '//resources/cr_elements/cr_toast/cr_toast.js';
import '//resources/cr_elements/cr_toolbar/cr_toolbar.js';
import '//resources/cr_elements/cr_button/cr_button.js';
import '//resources/cr_elements/cr_link_row/cr_link_row.js';
import '//resources/cr_elements/cr_shared_style.css.js';
import '//resources/cr_elements/cr_shared_vars.css.js';
import '//resources/cr_elements/cr_toggle/cr_toggle.js';
import '//resources/cr_elements/icons_lit.html.js';
import '//resources/cr_elements/cr_page_selector/cr_page_selector.js';
import '//resources/cr_elements/cr_menu_selector/cr_menu_selector.js';
import '//resources/cr_elements/cr_nav_menu_item_style.css.js';
import '//resources/cr_elements/cr_page_host_style.css.js';

import {CrContainerShadowMixin} from '//resources/cr_elements/cr_container_shadow_mixin.js';
import type {CrPageSelectorElement} from '//resources/cr_elements/cr_page_selector/cr_page_selector.js';
import type {CrToastElement} from '//resources/cr_elements/cr_toast/cr_toast.js';
import type {CrToggleElement} from '//resources/cr_elements/cr_toggle/cr_toggle.js';
import {I18nMixin} from '//resources/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from '//resources/js/assert.js';
import {focusWithoutInk} from '//resources/js/focus_without_ink.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import {PluralStringProxyImpl} from '//resources/js/plural_string_proxy.js';
import {PromiseResolver} from '//resources/js/promise_resolver.js';
import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {CertificateListV2Element} from './certificate_list_v2.js';
import {getTemplate} from './certificate_manager_v2.html.js';
import type {CertManagementMetadata, ImportResult} from './certificate_manager_v2.mojom-webui.js';
import {CertificateSource} from './certificate_manager_v2.mojom-webui.js';
import type {CertificatePasswordDialogElement} from './certificate_password_dialog.js';
import type {CertificateSubpageV2Element, SubpageCertificateList} from './certificate_subpage_v2.js';
import {CertificatesV2BrowserProxy} from './certificates_v2_browser_proxy.js';
import type {CrsSectionV2Element} from './crs_section_v2.js';
import type {Route} from './navigation_v2.js';
import {Page, RouteObserverMixin, Router} from './navigation_v2.js';

interface PasswordResult {
  password: string|null;
}

const CertificateManagerV2ElementBase =
    RouteObserverMixin(CrContainerShadowMixin(I18nMixin(PolymerElement)));

export interface CertificateManagerV2Element {
  $: {
    toolbar: HTMLElement,
    main: CrPageSelectorElement,
    platformClientCerts: CertificateListV2Element,
    // <if expr="is_win or is_macosx or is_linux">
    provisionedClientCerts: CertificateListV2Element,
    // </if>
    // <if expr="is_chromeos">
    extensionsClientCerts: CertificateListV2Element,
    // </if>
    toast: CrToastElement,
    importOsCerts: CrToggleElement,
    importOsCertsManagedIcon: HTMLElement,
    viewOsImportedCerts: HTMLElement,
    viewOsImportedClientCerts: HTMLElement,
    // <if expr="is_win or is_macosx">
    manageOsImportedCerts: HTMLElement,
    manageOsImportedClientCerts: HTMLElement,
    // </if>

    localMenuItem: HTMLElement,
    clientMenuItem: HTMLElement,
    crsMenuItem: HTMLElement,

    localCertSection: HTMLElement,
    clientCertSection: HTMLElement,
    crsCertSection: CrsSectionV2Element,
    adminCertsSection: CertificateSubpageV2Element,
    platformCertsSection: CertificateSubpageV2Element,
    platformClientCertsSection: CertificateSubpageV2Element,
    numSystemCerts: HTMLElement,
  };
}

export class CertificateManagerV2Element extends
    CertificateManagerV2ElementBase {
  static get is() {
    return 'certificate-manager-v2';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      selectedPage_: String,
      enterpriseSubpageLists_: {
        type: Array,
        value: () => {
          return [
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2TrustedCertsList'),
              certSource: CertificateSource.kEnterpriseTrustedCerts,
              hideExport: false,
              showImport: false,
            },
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2IntermediateCertsList'),
              certSource: CertificateSource.kEnterpriseIntermediateCerts,
              hideExport: false,
              showImport: false,
            },
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2DistrustedCertsList'),
              certSource: CertificateSource.kEnterpriseDistrustedCerts,
              hideExport: false,
              showImport: false,
            },
          ];
        },
      },
      platformSubpageLists_: {
        type: Array,
        value: () => {
          return [
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2TrustedCertsList'),
              certSource: CertificateSource.kPlatformUserTrustedCerts,
              hideExport: false,
              showImport: false,
            },
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2IntermediateCertsList'),
              certSource: CertificateSource.kPlatformUserIntermediateCerts,
              hideExport: false,
              showImport: false,
            },
            {
              headerText: loadTimeData.getString(
                  'certificateManagerV2DistrustedCertsList'),
              certSource: CertificateSource.kPlatformUserDistrustedCerts,
              hideExport: false,
              showImport: false,
            },
          ];
        },
      },

      clientPlatformSubpageLists_: {
        type: Array,
        computed: 'computeClientPlatformSubpageLists_(showClientCertImport_)',
      },

      toastMessage_: String,
      numSystemCertsString_: String,
      numPolicyCertsString_: String,

      showInfoDialog_: Boolean,
      infoDialogTitle_: String,
      infoDialogMessage_: String,
      showPasswordDialog_: Boolean,

      showSearch_: {
        type: Boolean,
        value: false,
      },

      importOsCertsEnabled_: {
        type: Boolean,
        computed: 'computeImportOsCertsEnabled_(certManagementMetadata_)',
      },

      importOsCertsEnabledManaged_: {
        type: Boolean,
        computed: 'computeImportOsCertsManaged_(certManagementMetadata_)',
      },

      showClientCertImport_: Boolean,

      certificateSourceEnum_: {
        type: Object,
        value: CertificateSource,
      },

      pageEnum_: {
        type: Object,
        value: Page,
      },
    };
  }

  private selectedPage_: Page;
  private toastMessage_: string;
  private showInfoDialog_: boolean = false;
  private infoDialogTitle_: string;
  private infoDialogMessage_: string;
  private showPasswordDialog_: boolean = false;
  private passwordEntryResolver_: PromiseResolver<PasswordResult>|null = null;
  private numPolicyCertsString_: string;
  private numSystemCertsString_: string;
  private certManagementMetadata_: CertManagementMetadata;
  private importOsCertsEnabled_: boolean;
  private importOsCertsEnabledManaged_: boolean;
  private enterpriseSubpageLists_: SubpageCertificateList[];
  private platformSubpageLists_: SubpageCertificateList[];
  private clientPlatformSubpageLists_: SubpageCertificateList[];
  // <if expr="not chromeos_ash">
  private showClientCertImport_: boolean = false;
  // </if>
  // <if expr="chromeos_ash">
  // TODO(crbug.com/40928765): Import should also be disabled in kiosk mode or
  // when disabled by policy.
  private showClientCertImport_: boolean = true;
  // </if>

  override ready() {
    super.ready();
    const proxy = CertificatesV2BrowserProxy.getInstance();
    proxy.callbackRouter.askForImportPassword.addListener(
        this.onAskForImportPassword_.bind(this));
    proxy.handler.getCertManagementMetadata().then(
        (results: {metadata: CertManagementMetadata}) => {
          this.certManagementMetadata_ = results.metadata;
          this.updateNumCertsStrings_();
        });
  }

  private onAskForImportPassword_(): Promise<PasswordResult> {
    this.showPasswordDialog_ = true;
    assert(this.passwordEntryResolver_ === null);
    this.passwordEntryResolver_ = new PromiseResolver<PasswordResult>();
    return this.passwordEntryResolver_.promise;
  }

  private onPasswordDialogClose_() {
    const passwordDialog =
        this.shadowRoot!.querySelector<CertificatePasswordDialogElement>(
            '#passwordDialog');
    assert(passwordDialog);
    assert(this.passwordEntryResolver_);
    this.passwordEntryResolver_.resolve({password: passwordDialog.value()});
    this.passwordEntryResolver_ = null;
    this.showPasswordDialog_ = false;
  }

  private updateNumCertsStrings_() {
    if (this.certManagementMetadata_ === undefined) {
      this.numPolicyCertsString_ = '';
      this.numSystemCertsString_ = '';
    } else {
      PluralStringProxyImpl.getInstance()
          .getPluralString(
              'certificateManagerV2NumCerts',
              this.certManagementMetadata_.numPolicyCerts)
          .then(label => {
            this.numPolicyCertsString_ = label;
          });
      PluralStringProxyImpl.getInstance()
          .getPluralString(
              'certificateManagerV2NumCerts',
              this.certManagementMetadata_.numUserAddedSystemCerts)
          .then(label => {
            this.numSystemCertsString_ = label;
          });
    }
  }

  private onHashCopied_() {
    this.toastMessage_ =
        loadTimeData.getString('certificateManagerV2HashCopiedToast');
    this.$.toast.show();
  }

  // Prevent clicks on sidebar items from navigating and therefore reloading
  // the page.
  protected onMenuItemClick_(e: MouseEvent) {
    e.preventDefault();
  }

  override async currentRouteChanged(route: Route, oldRoute: Route) {
    this.selectedPage_ = route.page;

    if (route.isSubpage()) {
      // Sub-pages always show the top shadow, regardless of scroll position.
      this.enableScrollObservation(false);
      this.setForceDropShadows(true);
    } else {
      // Main page uses scroll position to determine whether a shadow should
      // be shown.
      this.enableScrollObservation(true);
      this.setForceDropShadows(false);
    }

    if (route.isSubpage()) {
      await this.$.main.updateComplete;
      switch (route.page) {
        case Page.ADMIN_CERTS:
          this.$.adminCertsSection.setInitialFocus();
          break;
        case Page.PLATFORM_CERTS:
          this.$.platformCertsSection.setInitialFocus();
          break;
        case Page.PLATFORM_CLIENT_CERTS:
          this.$.platformClientCertsSection.setInitialFocus();
          break;
        default:
          assertNotReached();
      }
    } else if (oldRoute.isSubpage()) {
      // If we're navigating back from a subpage, we may need to fiddle
      // with the focus element if we're going back to its parent page.
      switch (oldRoute.page) {
        case Page.ADMIN_CERTS:
          if (route.page === Page.LOCAL_CERTS) {
            await this.$.main.updateComplete;
            const linkRow = this.shadowRoot!.querySelector<HTMLElement>(
                '#adminCertsInstalledLinkRow');
            assert(linkRow);
            focusWithoutInk(linkRow);
          }
          break;
        case Page.PLATFORM_CERTS:
          if (route.page === Page.LOCAL_CERTS) {
            await this.$.main.updateComplete;
            focusWithoutInk(this.$.viewOsImportedCerts);
          }
          break;
        case Page.PLATFORM_CLIENT_CERTS:
          if (route.page === Page.CLIENT_CERTS) {
            await this.$.main.updateComplete;
            focusWithoutInk(this.$.viewOsImportedClientCerts);
          }
          break;
        default:
          assertNotReached();
      }
    }
  }

  private onMenuItemActivate_(e: CustomEvent<{item: HTMLElement}>) {
    const page = e.detail.item.getAttribute('href');
    assert(page, 'Page is not available');
    Router.getInstance().navigateTo(page.substring(1) as Page);
  }

  private getSelectedTopLevelHref_(): string {
    switch (this.selectedPage_) {
      case Page.ADMIN_CERTS:
      case Page.PLATFORM_CERTS:
        return this.generateHrefForPage_(Page.LOCAL_CERTS);
      case Page.PLATFORM_CLIENT_CERTS:
        return this.generateHrefForPage_(Page.CLIENT_CERTS);
      default:
        return this.generateHrefForPage_(this.selectedPage_);
    }
  }

  private generateHrefForPage_(p: Page): string {
    return '/' + p;
  }

  private async onPlatformCertsLinkRowClick_(e: Event) {
    e.preventDefault();
    Router.getInstance().navigateTo(Page.PLATFORM_CERTS);
  }

  private async onClientPlatformCertsLinkRowClick_(e: Event) {
    e.preventDefault();
    Router.getInstance().navigateTo(Page.PLATFORM_CLIENT_CERTS);
  }

  private async onAdminCertsInstalledLinkRowClick_(e: Event) {
    e.preventDefault();
    Router.getInstance().navigateTo(Page.ADMIN_CERTS);
  }

  private onImportResult_(e: CustomEvent<ImportResult|null>) {
    const result = e.detail;
    if (result === null) {
      return;
    }
    if (result.error !== undefined) {
      // TODO(crbug.com/40928765): localize
      this.infoDialogTitle_ = 'import result';
      this.infoDialogMessage_ = result.error;
      this.showInfoDialog_ = true;
    }
  }

  private onInfoDialogClose_() {
    this.showInfoDialog_ = false;
  }

  private computeImportOsCertsEnabled_(): boolean {
    return this.certManagementMetadata_.includeSystemTrustStore;
  }

  private computeImportOsCertsManaged_(): boolean {
    return this.certManagementMetadata_.isIncludeSystemTrustStoreManaged;
  }

  private computeClientPlatformSubpageLists_(): SubpageCertificateList[] {
    return [
      {
        headerText: loadTimeData.getString(
            'certificateManagerV2ClientCertsFromPlatform'),
        certSource: CertificateSource.kPlatformClientCert,
        hideExport: true,
        showImport: this.showClientCertImport_,
      },
    ];
  }

  // If true, show the Custom Certs section.
  private showCustomSection_(): boolean {
    return this.certManagementMetadata_ !== undefined &&
        this.certManagementMetadata_.numPolicyCerts > 0;
  }

  // <if expr="is_win or is_macosx">
  private onManageCertsExternal_() {
    const proxy = CertificatesV2BrowserProxy.getInstance();
    proxy.handler.showNativeManageCertificates();
  }
  // </if>
}

declare global {
  interface HTMLElementTagNameMap {
    'certificate-manager-v2': CertificateManagerV2Element;
  }
}

customElements.define(
    CertificateManagerV2Element.is, CertificateManagerV2Element);