"""This module contains classes and functions used by all help pages."""
from __future__ import annotations
import abc
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
from django.http import Http404
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _non_lazy
from django.utils.translation import gettext_lazy as _
from help_pages.commands import KeyGenCommandBuilder
from help_pages.help_section import HelpRow, HelpSection, ValueRenderType
from pki.models.domain import DomainAllowedCertificateProfileModel
from pki.models.truststore import ActiveTrustpointTlsServerCredentialModel
if TYPE_CHECKING:
from trustpoint_core import oid
from devices.models import DeviceModel
from pki.models import CredentialModel, DevIdRegistration
from pki.models.domain import DomainModel
# --------------------------------------------------- Base Classes ----------------------------------------------------
[docs]
class HelpPageStrategy(abc.ABC):
"""Abstract base class for help page strategies."""
@abc.abstractmethod
[docs]
def build_sections(self, help_context: HelpContext) -> tuple[list[HelpSection], str]:
"""Builds the required sections."""
@dataclass(frozen=True)
[docs]
class HelpContext:
"""Holds shared context data."""
[docs]
domain: DomainModel
[docs]
domain_unique_name: str
[docs]
allowed_app_profiles: list[DomainAllowedCertificateProfileModel]
[docs]
public_key_info: oid.PublicKeyInfo
[docs]
host_base: str # https://IP:PORT
[docs]
host_cmp_path: str # {host_base}/.well-known/cmp/p/{domain.unique_name}
[docs]
host_est_path: str # {host_base}/.well-known/est/{domain.unique_name}
[docs]
cred_count: int # Running number to avoid overriding files on the client side
[docs]
device: None | DeviceModel = None
[docs]
devid_registration: None | DevIdRegistration = None
[docs]
def get_device_or_http_404(self) -> DeviceModel:
"""Gets the device or throws an HTTP404 error.
Raises:
Http404: If the device does not exist.
Returns:
The DeviceModel.
"""
if self.device is None:
err_msg = 'Device not found.'
raise Http404(err_msg)
return self.device
[docs]
def get_devid_registration_or_http_404(self) -> DevIdRegistration:
"""Gets the DevidRegistration or throws an HTTP404 error.
Raises:
Http404: If the DevidRegistration does not exist.
Returns:
The DevidRegistration.
"""
if self.devid_registration is None:
err_msg = 'DevidRegistration not found.'
raise Http404(err_msg)
return self.devid_registration
# ----------------------------------------- Reusable section build functions ------------------------------------------
[docs]
def build_keygen_section(help_context: HelpContext, file_name: str) -> HelpSection:
"""Builds the key-generation section.
Args:
help_context: The help context which will
file_name: The file_name to use if the default shall not be used. Defaults to None.
Returns:
The key-generation section.
"""
cmd = KeyGenCommandBuilder.get_key_gen_command(
public_key_info=help_context.public_key_info, cred_number=help_context.cred_count, key_name=file_name
)
return HelpSection(
_non_lazy('Key Generation'), [HelpRow(_non_lazy('Generate Key-Pair'), cmd, ValueRenderType.CODE)]
)
[docs]
def build_profile_select_section(app_cert_profiles: list[DomainAllowedCertificateProfileModel]) -> HelpSection:
"""Builds the profile select section.
Returns:
The profile select section.
"""
options = mark_safe('')
profile_list = DomainAllowedCertificateProfileModel.get_list_of_display_names(app_cert_profiles)
for i, (_profile_id, display_name, unique_name) in enumerate(profile_list):
options += format_html(
'<option value="{}"{}>{}</option>',
unique_name,
' selected' if i == 0 else '',
display_name,
)
if not options:
options = format_html('<option value="" selected disabled>{}</option>',
_('No application certificate profiles allowed in domain.'))
select = format_html(
'<select id="cert-profile-select" class="form-select" aria-label="Certificate Profile Select">{}</select>',
options,
)
return HelpSection(
_non_lazy('Certificate Profile Selection'),
[HelpRow(_non_lazy('Certificate Profile'), select, ValueRenderType.PLAIN)],
)
[docs]
def build_tls_trust_store_section() -> HelpSection:
"""Builds the TLS trust-store section.
Raises:
Http404: If no active Trustpoint TLS-server credential is found or the root CA cert is missing.
Returns:
The TLS trust-store section.
"""
tls = ActiveTrustpointTlsServerCredentialModel.objects.first()
if not tls or not tls.credential:
msg = format_html(
'<div class="tp-message alert alert-danger d-flex" role="alert">'
'<svg class="bi flex-shrink-0 tp-msg-icon-margin" width="20" height="20" '
'fill="currentColor" role="img" aria-label="error:">'
'<use xlink:href="/static/img/icons.svg#icon-error"></use></svg>'
'<div>{}<br>{}</div>'
'</div>',
'This onboarding method is not securely available without TLS.',
'Please ensure you are running Trustpoint within the correctly configured Docker environment!',
)
return HelpSection(
_non_lazy('Download TLS Trust-Store'),
[HelpRow(_non_lazy('Truststore is unavailable!'), msg, ValueRenderType.PLAIN)],
)
root = tls.credential.get_last_in_chain()
if not root:
raise Http404(_('Root CA certificate is missing.'))
url = reverse(
'pki:certificate-file-download-file-name',
kwargs={'file_format': 'pem', 'pk': root.pk, 'file_name': 'trustpoint-tls-trust-store.pem'},
)
btn = format_html('<a class="btn btn-primary w-100" href="{}">{}</a>', url, _('Download TLS Trust-Store'))
return HelpSection(
_non_lazy('Download TLS Trust-Store'),
[HelpRow(_non_lazy('Download TLS Trust-Store'), btn, ValueRenderType.PLAIN)],
)
[docs]
def build_cmp_signer_trust_store_section(domain: DomainModel) -> HelpSection:
"""Builds the CMP-signer trust-store section.
Returns:
The CMP-signer trust-store section.
"""
issuing_ca = domain.issuing_ca
if not issuing_ca:
err_msg = 'Issuing CA not configured'
raise ValueError(err_msg)
root_ca_model = cast('CredentialModel', issuing_ca.credential).get_last_in_chain()
if not root_ca_model:
err_msg = 'No Root CA certificate found.'
raise ValueError(err_msg)
cmp_signer_pk = root_ca_model.pk
download_tls_truststore_row = HelpRow(
key=_non_lazy('Download CMP-Signer Trust-Store'),
value=format_html(
'<a href="{}" class="btn btn-primary w-100">{}</a>',
reverse(
'pki:certificate-file-download-file-name',
kwargs={'file_format': 'pem', 'pk': cmp_signer_pk, 'file_name': 'domain-credential-full-chain.pem'},
),
_non_lazy('Download CMP-Signer Trust-Store'),
),
value_render_type=ValueRenderType.PLAIN,
)
return HelpSection(heading=_non_lazy('Download CMP-Signer Trust-Store'), rows=[download_tls_truststore_row])
[docs]
def build_issuing_ca_cert_section(domain: DomainModel) -> HelpSection:
"""Builds the Issuing CA Certificate section.
Returns:
The Issuing CA Certificate section.
"""
issuing_ca = domain.issuing_ca
if not issuing_ca:
err_msg = 'Issuing CA not configured'
raise ValueError(err_msg)
issuing_ca_pk = issuing_ca.pk
download_issuing_ca_cert_row = HelpRow(
key=_non_lazy('Download Issuing CA Certificate'),
value=format_html(
'<a href="{}" class="btn btn-primary w-100">{}</a>',
reverse(
'pki:certificate-file-download-file-name',
kwargs={'file_format': 'pem', 'pk': issuing_ca_pk, 'file_name': 'issuing_ca_cert.pem'},
),
_non_lazy('Download Issuing CA Certificate'),
),
value_render_type=ValueRenderType.PLAIN,
)
return HelpSection(heading=_non_lazy('Download Issuing CA Certificate'), rows=[download_issuing_ca_cert_row])