"""Views for Issuing CA management."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db.models import ProtectedError
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic.detail import DetailView
from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from pki.forms import (
IssuingCaAddFileImportPkcs12Form,
IssuingCaAddFileImportSeparateFilesForm,
IssuingCaAddMethodSelectForm,
)
from pki.models import CertificateModel, IssuingCaModel
from trustpoint.settings import UIConfig
from trustpoint.views.base import (
BulkDeleteView,
ContextDataMixin,
SortableTableMixin,
)
from trustpoint.logger import LoggerMixin
if TYPE_CHECKING:
from django.db.models import QuerySet
from django.forms import Form
from django.http import HttpRequest
[docs]
class IssuingCaContextMixin(ContextDataMixin):
"""Mixin which adds context_data for the PKI -> Issuing CAs pages."""
[docs]
context_page_category = 'pki'
[docs]
context_page_name = 'issuing_cas'
[docs]
class IssuingCaTableView(IssuingCaContextMixin, SortableTableMixin, ListView[IssuingCaModel]):
"""Issuing CA Table View."""
[docs]
template_name = 'pki/issuing_cas/issuing_cas.html' # Template file
[docs]
context_object_name = 'issuing_ca'
[docs]
paginate_by = UIConfig.paginate_by # Number of items per page
[docs]
default_sort_param = 'unique_name'
[docs]
class IssuingCaAddMethodSelectView(IssuingCaContextMixin, FormView[IssuingCaAddMethodSelectForm]):
"""View to select the method to add an Issuing CA."""
[docs]
template_name = 'pki/issuing_cas/add/method_select.html'
[docs]
class IssuingCaAddFileImportPkcs12View(IssuingCaContextMixin, FormView[IssuingCaAddFileImportPkcs12Form]):
"""View to import an Issuing CA from a PKCS12 file."""
[docs]
template_name = 'pki/issuing_cas/add/file_import.html'
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
class IssuingCaAddFileImportSeparateFilesView(IssuingCaContextMixin, FormView[IssuingCaAddFileImportSeparateFilesForm]):
"""View to import an Issuing CA from separate PEM files."""
[docs]
template_name = 'pki/issuing_cas/add/file_import.html'
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
class IssuingCaConfigView(LoggerMixin, IssuingCaContextMixin, DetailView[IssuingCaModel]):
"""View to display the details of an Issuing CA."""
[docs]
http_method_names = ('get',)
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
ignore_url = reverse_lazy('pki:issuing_cas')
[docs]
template_name = 'pki/issuing_cas/config.html'
[docs]
context_object_name = 'issuing_ca'
[docs]
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""Adds the issued certificates to the context.
Args:
**kwargs: Keyword arguments passed to super().get_context_data()
Returns:
The context to render the page.
"""
context = super().get_context_data(**kwargs)
issuing_ca = self.get_object()
issued_certificates = CertificateModel.objects.filter(
issuer_public_bytes=issuing_ca.credential.certificate.subject_public_bytes
)
context['issued_certificates'] = issued_certificates
return context
[docs]
class IssuedCertificatesListView(IssuingCaContextMixin, ListView[CertificateModel]):
"""View to display all certificates issued by a specific Issuing CA."""
[docs]
model = CertificateModel
[docs]
template_name = 'pki/issuing_cas/issued_certificates.html'
[docs]
context_object_name = 'issued_certificates'
[docs]
def get_queryset(self) -> QuerySet[CertificateModel, CertificateModel]:
"""Gets the required and filtered QuerySet.
Returns:
The filtered QuerySet.
"""
issuing_ca = get_object_or_404(IssuingCaModel, pk=self.kwargs['pk'])
# PyCharm TypeChecker issue - this passes mypy
# noinspection PyTypeChecker
# TODO(AlexHx8472): This is not a good query. Use issued credentials to get the certificates.
return CertificateModel.objects.filter(
issuer_public_bytes=issuing_ca.credential.certificate.subject_public_bytes
)
[docs]
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""Adds the issuing ca model object to the context.
Args:
**kwargs: Keyword arguments passed to super().get_context_data()
Returns:
The context to render the page.
"""
context = super().get_context_data(**kwargs)
context['issuing_ca'] = get_object_or_404(IssuingCaModel, pk=self.kwargs['pk'])
return context
[docs]
class IssuingCaDetailView(IssuingCaContextMixin, DetailView[IssuingCaModel]):
"""Detail view for an Issuing CA."""
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
ignore_url = reverse_lazy('pki:issuing_cas')
[docs]
template_name = 'pki/issuing_cas/details.html'
[docs]
context_object_name = 'issuing_ca'
[docs]
class IssuingCaBulkDeleteConfirmView(IssuingCaContextMixin, BulkDeleteView):
"""View to confirm the deletion of multiple Issuing CAs."""
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
ignore_url = reverse_lazy('pki:issuing_cas')
[docs]
template_name = 'pki/issuing_cas/confirm_delete.html'
[docs]
context_object_name = 'issuing_cas'
[docs]
class IssuingCaCrlGenerationView(IssuingCaContextMixin, DetailView[IssuingCaModel]):
"""View to manually generate a CRL for an Issuing CA."""
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
ignore_url = reverse_lazy('pki:issuing_cas')
[docs]
context_object_name = 'issuing_ca'
[docs]
http_method_names = ('get',)
# TODO(Air): This view should use a POST request as it is an action. # noqa: FIX002
# However, this is not trivial in the config view as that already contains a form.
[docs]
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
"""Generate a CRL for the Issuing CA (should be POST!)."""
del args
del kwargs
issuing_ca = self.get_object()
if issuing_ca.issue_crl():
messages.success(request, _('CRL for Issuing CA %s has been generated.') % issuing_ca.unique_name)
else:
messages.error(request, _('Failed to generate CRL for Issuing CA %s.') % issuing_ca.unique_name)
return redirect('pki:issuing_cas-config', pk=issuing_ca.pk)
[docs]
class CrlDownloadView(IssuingCaContextMixin, DetailView[IssuingCaModel]):
"""Unauthenticated view to download the certificate revocation list of an Issuing CA."""
[docs]
http_method_names = ('get',)
[docs]
success_url = reverse_lazy('pki:issuing_cas')
[docs]
ignore_url = reverse_lazy('pki:issuing_cas')
[docs]
context_object_name = 'issuing_ca'
[docs]
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
"""Download the CRL of the Issuing CA."""
del args
del kwargs
issuing_ca = self.get_object()
crl_pem = issuing_ca.crl_pem
if not crl_pem:
messages.warning(request, _('No CRL available for issuing CA %s.') % issuing_ca.unique_name)
return redirect('pki:issuing_cas')
response = HttpResponse(crl_pem, content_type='application/x-pem-file')
response['Content-Disposition'] = f'attachment; filename="{issuing_ca.unique_name}.crl"'
return response