"""TLS setting specific views."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, cast
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import transaction
from django.db.models import ProtectedError
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import FormView, TemplateView, View
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import filters, viewsets
from rest_framework.permissions import IsAuthenticated
from management.forms import IPv4AddressForm, TlsAddFileImportPkcs12Form, TlsAddFileImportSeparateFilesForm
from management.management.commands.update_tls import Command as UpdateTlsCommand
from management.models import TlsSettings
from management.serializer.credential import CredentialSerializer
from pki.models import CertificateModel, CredentialModel, GeneralNameIpAddress
from pki.models.truststore import ActiveTrustpointTlsServerCredentialModel
from setup_wizard.forms import StartupWizardTlsCertificateForm
from setup_wizard.tls_credential import TlsServerCredentialGenerator
from trustpoint.logger import LoggerMixin
from trustpoint.views.base import BulkDeleteView
if TYPE_CHECKING:
from django.forms import Form
from django.http import HttpRequest, HttpResponse
[docs]
class TlsSettingsContextMixin:
"""Mixin which adds data to the context for the TLS settings application."""
[docs]
page_category: str = 'management'
[docs]
page_name: str = 'tls'
[docs]
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""Add page_category and page_name to context."""
context = cast('dict[str, Any]', super().get_context_data(**kwargs)) # type: ignore[misc]
context['page_category'] = self.page_category
context['page_name'] = self.page_name
return context
[docs]
class TlsView(LoggerMixin, TlsSettingsContextMixin, FormView[IPv4AddressForm]):
"""View to display certificate details, including Subject Alternative Name (SAN) and associated IP addresses."""
[docs]
template_name = 'management/tls.html'
[docs]
success_url = reverse_lazy('management:tls')
[docs]
def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""Add certificate information, including SAN data and issuer details, to the context for display."""
context = super().get_context_data(**kwargs)
try:
active_credential = ActiveTrustpointTlsServerCredentialModel.objects.select_related('credential').get(id=1)
except ActiveTrustpointTlsServerCredentialModel.DoesNotExist:
active_credential = None
certificate = None
if active_credential and active_credential.credential:
certificate = active_credential.credential.certificate
san_ips = []
san_dns_names = []
issuer_details: dict[str, str | None] = {
'country': None,
'organization': None,
'common_name': None,
}
if certificate and certificate.subject_alternative_name_extension:
san_model = certificate.subject_alternative_name_extension.subject_alt_name
if san_model:
san_ips = [str(ip_entry.value) for ip_entry in san_model.ip_addresses.all()]
san_dns_names = [dns_entry.value for dns_entry in san_model.dns_names.all()]
if certificate and certificate.issuer.exists():
issuer_mapping = {
'2.5.4.6': 'country',
'2.5.4.10': 'organization',
'2.5.4.3': 'common_name',
}
for attribute in certificate.issuer.all():
if attribute.oid in issuer_mapping:
field = issuer_mapping[attribute.oid]
issuer_details[field] = attribute.value
tls_certificates = CertificateModel.objects.filter(
credential__credential_type=CredentialModel.CredentialTypeChoice.TRUSTPOINT_TLS_SERVER)
self.logger.info('TLS certificates count: %s', tls_certificates.count())
for cert in tls_certificates:
self.logger.info('TLS cert: %s, pk: %s', cert.common_name, cert.pk)
context.update({
'certificate': certificate,
'san_ips': san_ips,
'san_dns_names': san_dns_names,
'issuer_details': issuer_details,
'tls_certificates': tls_certificates,
'tls_form': StartupWizardTlsCertificateForm()
})
return context
[docs]
def get_san_ips(self) -> list[str]:
"""Fetches IPv4 addresses from the Subject Alternative Name (SAN) extension of the active TLS certificate.
Returns:
A list of IPv4 addresses (as strings) or an empty list if none are found.
"""
try:
try:
active_credential = ActiveTrustpointTlsServerCredentialModel.objects.select_related('credential').get(
id=1)
except ActiveTrustpointTlsServerCredentialModel.DoesNotExist:
active_credential = None
certificate = None
if active_credential and active_credential.credential:
certificate = active_credential.credential.certificate
if not certificate or not certificate.subject_alternative_name_extension:
return []
san_model = certificate.subject_alternative_name_extension.subject_alt_name
if not san_model:
return []
ipv4_addresses = GeneralNameIpAddress.objects.filter(
general_names_set=san_model, ip_type=GeneralNameIpAddress.IpType.IPV4_ADDRESS
).values_list('value', flat=True)
return list(ipv4_addresses)
except ObjectDoesNotExist:
return []
[docs]
class TlsAddMethodSelectView(TemplateView):
"""View to select the method to add a TLS Certificate."""
[docs]
template_name = 'management/tls/method_select.html'
[docs]
success_url = reverse_lazy('management:tls-add-method-select')
[docs]
class GenerateTlsCertificateView(LoggerMixin, FormView[StartupWizardTlsCertificateForm]):
"""View for generating TLS Server Credentials in the setup wizard.
This view handles the generation of TLS Server Credentials. It provides a form for the
user to input necessary information such as IP addresses and domain names, and
processes the data to generate the required TLS certificates.
Attributes:
http_method_names (ClassVar[list[str]]): HTTP methods allowed for this view.
template_name (str): Path to the template used for rendering the form.
form_class (Form): The form class used to validate user input.
success_url (str): The URL to redirect to upon successful credential generation.
"""
[docs]
http_method_names = ('get', 'post')
[docs]
template_name = 'management/tls/generate_tls.html'
[docs]
success_url = reverse_lazy('management:tls')
[docs]
class TlsAddFileImportPkcs12View(TlsSettingsContextMixin, FormView[TlsAddFileImportPkcs12Form], LoggerMixin):
"""View to import an TLS-Server Credential from a PKCS12 file."""
[docs]
template_name = 'management/tls/file_import.html'
[docs]
success_url = reverse_lazy('management:tls')
[docs]
class TlsAddFileImportSeparateFilesView(
TlsSettingsContextMixin, FormView[TlsAddFileImportSeparateFilesForm], LoggerMixin
):
"""View to import an Issuing CA from separate PEM files."""
[docs]
template_name = 'management/tls/file_import.html'
[docs]
success_url = reverse_lazy('management:tls')
[docs]
class TlsBulkDeleteConfirmView(TlsSettingsContextMixin, BulkDeleteView):
"""View to confirm the deletion of multiple TLS certificates."""
[docs]
model = CertificateModel
[docs]
success_url = reverse_lazy('management:tls')
[docs]
ignore_url = reverse_lazy('management:tls')
[docs]
template_name = 'management/tls/confirm_delete.html'
[docs]
context_object_name = 'certificates'
[docs]
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
"""Handle GET requests."""
queryset = self.get_queryset()
if not queryset.exists():
messages.error(request, _('No TLs certificates selected for deletion.'))
return HttpResponseRedirect(self.success_url)
return super().get(request, *args, **kwargs)
[docs]
class ActivateTlsServerView(View, LoggerMixin):
"""Activate a TLS server certificate."""
[docs]
def post(self, request: HttpRequest, *args: Any, **kwargs: dict[str, Any]) -> HttpResponse:
"""Handle a valid form submission for TLS Server Credential activation."""
del args
cert_id: int = kwargs['pk'] # type: ignore[assignment]
self.logger.info('Activating TLS certificate with ID: %s', cert_id)
try:
tls_certificate = CredentialModel.objects.get(
certificate__id=cert_id)
self.logger.info('Found TLS credential: %s', tls_certificate.id)
active_tls, _ = ActiveTrustpointTlsServerCredentialModel.objects.get_or_create(id=1)
active_tls.credential = tls_certificate
active_tls.save()
UpdateTlsCommand().handle() # Apply new NGINX TLS configuration
if tls_certificate.certificate is None:
self.logger.error('TLS certificate has no certificate model')
messages.error(request, 'TLS certificate has no certificate model')
return redirect(reverse('management:tls'))
self.logger.info(
'Activated TLS credential: %s, certificate: %s',
tls_certificate.id, tls_certificate.certificate.id
)
messages.success(request, 'TLS Server certificate activated successfully')
except (CredentialModel.DoesNotExist, ValidationError):
self.logger.exception('Failed to activate TLS certificate')
messages.error(request, 'Failed to activate TLS certificate')
except Exception:
self.logger.exception('Unexpected error activating TLS certificate')
messages.error(request, 'An unexpected error occurred while activating TLS certificate')
return redirect(reverse('management:tls'))
@extend_schema(tags=['Tls'])
@extend_schema_view(
list=extend_schema(description='Retrieve a list of all TLS Certificates.'),
retrieve=extend_schema(description='Retrieve a single TLS Certificate by id.'),
create=extend_schema(description='Create a TLS Certificate.'),
update=extend_schema(description='Update an existing TLS Certificate.'),
partial_update=extend_schema(description='Partially update an existing TLS Certificate.'),
destroy=extend_schema(description='Delete a TLS Certificate.')
)
[docs]
class TlsViewSet(viewsets.ModelViewSet[Any]):
"""ViewSet for managing Backup instances.
Supports standard CRUD operations such as list, retrieve,
create, update, and delete.
"""
[docs]
queryset = CredentialModel.objects.all().order_by('-created_at')
[docs]
serializer_class = CredentialSerializer
[docs]
permission_classes = (IsAuthenticated,)
[docs]
filter_backends = (
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
)
[docs]
filterset_fields: ClassVar = ['credential_type']
[docs]
search_fields: ClassVar = ['certificates']
[docs]
ordering_fields: ClassVar = ['created_at']