We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Certificate requires private key to be used for LDAP server verification in 2023.8.1
**Describe the bug**
CA Certificates that don't have private keys seem to be not showing up in the list of certificates for the configuration of TLS verification for LDAP sources

**To Reproduce**
Steps to reproduce the behavior:

1. Import the CA certificate of an LDAP server
2. Configure an LDAP source for that LDAP server
3. During configuration, try to select the CA certificate imported earlier as "TLS Verification Certificate"
4. In the list of certificates, only those with a private key are visible. I obviously don't have the private key of the CA, and I wouldn't want to import it here even if I had it, for security reasons.

**Expected behavior**
Certificates without private key can be selected if the purpose is only to validate remote servers.

**Screenshots**

![image](https://github.com/goauthentik/authentik/assets/359918/e8f96bde-086f-4a2e-b09c-cd59ab3e5d0c)
![image](https://github.com/goauthentik/authentik/assets/359918/24aabdfe-5f49-4b78-8d4a-3541717e968c)

**Logs**
N/A

**Version and Deployment (please complete the following information):**

-   authentik version: 2023.8.1
-   Deployment: docker-compose

**Additional context**
N/A


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/crypto/api.py`
Content:
```
1 """Crypto API Views"""
2 from datetime import datetime
3 from typing import Optional
4 
5 from cryptography.hazmat.backends import default_backend
6 from cryptography.hazmat.primitives.serialization import load_pem_private_key
7 from cryptography.x509 import load_pem_x509_certificate
8 from django.http.response import HttpResponse
9 from django.urls import reverse
10 from django.utils.translation import gettext_lazy as _
11 from django_filters import FilterSet
12 from django_filters.filters import BooleanFilter
13 from drf_spectacular.types import OpenApiTypes
14 from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
15 from rest_framework.decorators import action
16 from rest_framework.exceptions import ValidationError
17 from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
18 from rest_framework.filters import OrderingFilter, SearchFilter
19 from rest_framework.request import Request
20 from rest_framework.response import Response
21 from rest_framework.serializers import ModelSerializer
22 from rest_framework.viewsets import ModelViewSet
23 from structlog.stdlib import get_logger
24 
25 from authentik.api.authorization import SecretKeyFilter
26 from authentik.api.decorators import permission_required
27 from authentik.core.api.used_by import UsedByMixin
28 from authentik.core.api.utils import PassiveSerializer
29 from authentik.crypto.apps import MANAGED_KEY
30 from authentik.crypto.builder import CertificateBuilder
31 from authentik.crypto.models import CertificateKeyPair
32 from authentik.events.models import Event, EventAction
33 
34 LOGGER = get_logger()
35 
36 
37 class CertificateKeyPairSerializer(ModelSerializer):
38     """CertificateKeyPair Serializer"""
39 
40     fingerprint_sha256 = SerializerMethodField()
41     fingerprint_sha1 = SerializerMethodField()
42 
43     cert_expiry = SerializerMethodField()
44     cert_subject = SerializerMethodField()
45     private_key_available = SerializerMethodField()
46     private_key_type = SerializerMethodField()
47 
48     certificate_download_url = SerializerMethodField()
49     private_key_download_url = SerializerMethodField()
50 
51     @property
52     def _should_include_details(self) -> bool:
53         request: Request = self.context.get("request", None)
54         if not request:
55             return True
56         return str(request.query_params.get("include_details", "true")).lower() == "true"
57 
58     def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> Optional[str]:
59         "Get certificate Hash (SHA256)"
60         if not self._should_include_details:
61             return None
62         return instance.fingerprint_sha256
63 
64     def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> Optional[str]:
65         "Get certificate Hash (SHA1)"
66         if not self._should_include_details:
67             return None
68         return instance.fingerprint_sha1
69 
70     def get_cert_expiry(self, instance: CertificateKeyPair) -> Optional[datetime]:
71         "Get certificate expiry"
72         if not self._should_include_details:
73             return None
74         return DateTimeField().to_representation(instance.certificate.not_valid_after)
75 
76     def get_cert_subject(self, instance: CertificateKeyPair) -> Optional[str]:
77         """Get certificate subject as full rfc4514"""
78         if not self._should_include_details:
79             return None
80         return instance.certificate.subject.rfc4514_string()
81 
82     def get_private_key_available(self, instance: CertificateKeyPair) -> bool:
83         """Show if this keypair has a private key configured or not"""
84         return instance.key_data != "" and instance.key_data is not None
85 
86     def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:
87         """Get the private key's type, if set"""
88         if not self._should_include_details:
89             return None
90         key = instance.private_key
91         if key:
92             return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
93         return None
94 
95     def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
96         """Get URL to download certificate"""
97         return (
98             reverse(
99                 "authentik_api:certificatekeypair-view-certificate",
100                 kwargs={"pk": instance.pk},
101             )
102             + "?download"
103         )
104 
105     def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:
106         """Get URL to download private key"""
107         return (
108             reverse(
109                 "authentik_api:certificatekeypair-view-private-key",
110                 kwargs={"pk": instance.pk},
111             )
112             + "?download"
113         )
114 
115     def validate_certificate_data(self, value: str) -> str:
116         """Verify that input is a valid PEM x509 Certificate"""
117         try:
118             # Cast to string to fully load and parse certificate
119             # Prevents issues like https://github.com/goauthentik/authentik/issues/2082
120             str(load_pem_x509_certificate(value.encode("utf-8"), default_backend()))
121         except ValueError as exc:
122             LOGGER.warning("Failed to load certificate", exc=exc)
123             raise ValidationError("Unable to load certificate.")
124         return value
125 
126     def validate_key_data(self, value: str) -> str:
127         """Verify that input is a valid PEM Key"""
128         # Since this field is optional, data can be empty.
129         if value != "":
130             try:
131                 # Cast to string to fully load and parse certificate
132                 # Prevents issues like https://github.com/goauthentik/authentik/issues/2082
133                 str(
134                     load_pem_private_key(
135                         str.encode("\n".join([x.strip() for x in value.split("\n")])),
136                         password=None,
137                         backend=default_backend(),
138                     )
139                 )
140             except (ValueError, TypeError) as exc:
141                 LOGGER.warning("Failed to load private key", exc=exc)
142                 raise ValidationError("Unable to load private key (possibly encrypted?).")
143         return value
144 
145     class Meta:
146         model = CertificateKeyPair
147         fields = [
148             "pk",
149             "name",
150             "fingerprint_sha256",
151             "fingerprint_sha1",
152             "certificate_data",
153             "key_data",
154             "cert_expiry",
155             "cert_subject",
156             "private_key_available",
157             "private_key_type",
158             "certificate_download_url",
159             "private_key_download_url",
160             "managed",
161         ]
162         extra_kwargs = {
163             "managed": {"read_only": True},
164             "key_data": {"write_only": True},
165             "certificate_data": {"write_only": True},
166         }
167 
168 
169 class CertificateDataSerializer(PassiveSerializer):
170     """Get CertificateKeyPair's data"""
171 
172     data = CharField(read_only=True)
173 
174 
175 class CertificateGenerationSerializer(PassiveSerializer):
176     """Certificate generation parameters"""
177 
178     common_name = CharField()
179     subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
180     validity_days = IntegerField(initial=365)
181 
182 
183 class CertificateKeyPairFilter(FilterSet):
184     """Filter for certificates"""
185 
186     has_key = BooleanFilter(
187         label="Only return certificate-key pairs with keys", method="filter_has_key"
188     )
189 
190     def filter_has_key(self, queryset, name, value):  # pragma: no cover
191         """Only return certificate-key pairs with keys"""
192         return queryset.exclude(key_data__exact="")
193 
194     class Meta:
195         model = CertificateKeyPair
196         fields = ["name", "managed"]
197 
198 
199 class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
200     """CertificateKeyPair Viewset"""
201 
202     queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
203     serializer_class = CertificateKeyPairSerializer
204     filterset_class = CertificateKeyPairFilter
205     ordering = ["name"]
206     search_fields = ["name"]
207     filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]
208 
209     @extend_schema(
210         parameters=[
211             # Override the type for `has_key` above
212             OpenApiParameter(
213                 "has_key",
214                 bool,
215                 required=False,
216                 description="Only return certificate-key pairs with keys",
217             ),
218             OpenApiParameter("include_details", bool, default=True),
219         ]
220     )
221     def list(self, request, *args, **kwargs):
222         return super().list(request, *args, **kwargs)
223 
224     @permission_required(None, ["authentik_crypto.add_certificatekeypair"])
225     @extend_schema(
226         request=CertificateGenerationSerializer(),
227         responses={
228             200: CertificateKeyPairSerializer,
229             400: OpenApiResponse(description="Bad request"),
230         },
231     )
232     @action(detail=False, methods=["POST"])
233     def generate(self, request: Request) -> Response:
234         """Generate a new, self-signed certificate-key pair"""
235         data = CertificateGenerationSerializer(data=request.data)
236         if not data.is_valid():
237             return Response(data.errors, status=400)
238         raw_san = data.validated_data.get("subject_alt_name", "")
239         sans = raw_san.split(",") if raw_san != "" else []
240         builder = CertificateBuilder(data.validated_data["common_name"])
241         builder.build(
242             subject_alt_names=sans,
243             validity_days=int(data.validated_data["validity_days"]),
244         )
245         instance = builder.save()
246         serializer = self.get_serializer(instance)
247         return Response(serializer.data)
248 
249     @extend_schema(
250         parameters=[
251             OpenApiParameter(
252                 name="download",
253                 location=OpenApiParameter.QUERY,
254                 type=OpenApiTypes.BOOL,
255             )
256         ],
257         responses={200: CertificateDataSerializer(many=False)},
258     )
259     @action(detail=True, pagination_class=None, filter_backends=[])
260     def view_certificate(self, request: Request, pk: str) -> Response:
261         """Return certificate-key pairs certificate and log access"""
262         certificate: CertificateKeyPair = self.get_object()
263         Event.new(  # noqa # nosec
264             EventAction.SECRET_VIEW,
265             secret=certificate,
266             type="certificate",
267         ).from_http(request)
268         if "download" in request.query_params:
269             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
270             response = HttpResponse(
271                 certificate.certificate_data, content_type="application/x-pem-file"
272             )
273             response[
274                 "Content-Disposition"
275             ] = f'attachment; filename="{certificate.name}_certificate.pem"'
276             return response
277         return Response(CertificateDataSerializer({"data": certificate.certificate_data}).data)
278 
279     @extend_schema(
280         parameters=[
281             OpenApiParameter(
282                 name="download",
283                 location=OpenApiParameter.QUERY,
284                 type=OpenApiTypes.BOOL,
285             )
286         ],
287         responses={200: CertificateDataSerializer(many=False)},
288     )
289     @action(detail=True, pagination_class=None, filter_backends=[])
290     def view_private_key(self, request: Request, pk: str) -> Response:
291         """Return certificate-key pairs private key and log access"""
292         certificate: CertificateKeyPair = self.get_object()
293         Event.new(  # noqa # nosec
294             EventAction.SECRET_VIEW,
295             secret=certificate,
296             type="private_key",
297         ).from_http(request)
298         if "download" in request.query_params:
299             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html
300             response = HttpResponse(certificate.key_data, content_type="application/x-pem-file")
301             response[
302                 "Content-Disposition"
303             ] = f'attachment; filename="{certificate.name}_private_key.pem"'
304             return response
305         return Response(CertificateDataSerializer({"data": certificate.key_data}).data)
306
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py
--- a/authentik/crypto/api.py
+++ b/authentik/crypto/api.py
@@ -189,6 +189,8 @@
 
     def filter_has_key(self, queryset, name, value):  # pragma: no cover
         """Only return certificate-key pairs with keys"""
+        if not value:
+            return queryset
         return queryset.exclude(key_data__exact="")
 
     class Meta:

{"golden_diff": "diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py\n--- a/authentik/crypto/api.py\n+++ b/authentik/crypto/api.py\n@@ -189,6 +189,8 @@\n \n     def filter_has_key(self, queryset, name, value):  # pragma: no cover\n         \"\"\"Only return certificate-key pairs with keys\"\"\"\n+        if not value:\n+            return queryset\n         return queryset.exclude(key_data__exact=\"\")\n \n     class Meta:\n", "issue": "Certificate requires private key to be used for LDAP server verification in 2023.8.1\n**Describe the bug**\r\nCA Certificates that don't have private keys seem to be not showing up in the list of certificates for the configuration of TLS verification for LDAP sources\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n\r\n1. Import the CA certificate of an LDAP server\r\n2. Configure an LDAP source for that LDAP server\r\n3. During configuration, try to select the CA certificate imported earlier as \"TLS Verification Certificate\"\r\n4. In the list of certificates, only those with a private key are visible. I obviously don't have the private key of the CA, and I wouldn't want to import it here even if I had it, for security reasons.\r\n\r\n**Expected behavior**\r\nCertificates without private key can be selected if the purpose is only to validate remote servers.\r\n\r\n**Screenshots**\r\n\r\n![image](https://github.com/goauthentik/authentik/assets/359918/e8f96bde-086f-4a2e-b09c-cd59ab3e5d0c)\r\n![image](https://github.com/goauthentik/authentik/assets/359918/24aabdfe-5f49-4b78-8d4a-3541717e968c)\r\n\r\n**Logs**\r\nN/A\r\n\r\n**Version and Deployment (please complete the following information):**\r\n\r\n-   authentik version: 2023.8.1\r\n-   Deployment: docker-compose\r\n\r\n**Additional context**\r\nN/A\r\n\n", "before_files": [{"content": "\"\"\"Crypto API Views\"\"\"\nfrom datetime import datetime\nfrom typing import Optional\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nfrom cryptography.x509 import load_pem_x509_certificate\nfrom django.http.response import HttpResponse\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\nfrom django_filters import FilterSet\nfrom django_filters.filters import BooleanFilter\nfrom drf_spectacular.types import OpenApiTypes\nfrom drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema\nfrom rest_framework.decorators import action\nfrom rest_framework.exceptions import ValidationError\nfrom rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField\nfrom rest_framework.filters import OrderingFilter, SearchFilter\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.serializers import ModelSerializer\nfrom rest_framework.viewsets import ModelViewSet\nfrom structlog.stdlib import get_logger\n\nfrom authentik.api.authorization import SecretKeyFilter\nfrom authentik.api.decorators import permission_required\nfrom authentik.core.api.used_by import UsedByMixin\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.crypto.apps import MANAGED_KEY\nfrom authentik.crypto.builder import CertificateBuilder\nfrom authentik.crypto.models import CertificateKeyPair\nfrom authentik.events.models import Event, EventAction\n\nLOGGER = get_logger()\n\n\nclass CertificateKeyPairSerializer(ModelSerializer):\n    \"\"\"CertificateKeyPair Serializer\"\"\"\n\n    fingerprint_sha256 = SerializerMethodField()\n    fingerprint_sha1 = SerializerMethodField()\n\n    cert_expiry = SerializerMethodField()\n    cert_subject = SerializerMethodField()\n    private_key_available = SerializerMethodField()\n    private_key_type = SerializerMethodField()\n\n    certificate_download_url = SerializerMethodField()\n    private_key_download_url = SerializerMethodField()\n\n    @property\n    def _should_include_details(self) -> bool:\n        request: Request = self.context.get(\"request\", None)\n        if not request:\n            return True\n        return str(request.query_params.get(\"include_details\", \"true\")).lower() == \"true\"\n\n    def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"Get certificate Hash (SHA256)\"\n        if not self._should_include_details:\n            return None\n        return instance.fingerprint_sha256\n\n    def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"Get certificate Hash (SHA1)\"\n        if not self._should_include_details:\n            return None\n        return instance.fingerprint_sha1\n\n    def get_cert_expiry(self, instance: CertificateKeyPair) -> Optional[datetime]:\n        \"Get certificate expiry\"\n        if not self._should_include_details:\n            return None\n        return DateTimeField().to_representation(instance.certificate.not_valid_after)\n\n    def get_cert_subject(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"\"\"Get certificate subject as full rfc4514\"\"\"\n        if not self._should_include_details:\n            return None\n        return instance.certificate.subject.rfc4514_string()\n\n    def get_private_key_available(self, instance: CertificateKeyPair) -> bool:\n        \"\"\"Show if this keypair has a private key configured or not\"\"\"\n        return instance.key_data != \"\" and instance.key_data is not None\n\n    def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"\"\"Get the private key's type, if set\"\"\"\n        if not self._should_include_details:\n            return None\n        key = instance.private_key\n        if key:\n            return key.__class__.__name__.replace(\"_\", \"\").lower().replace(\"privatekey\", \"\")\n        return None\n\n    def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:\n        \"\"\"Get URL to download certificate\"\"\"\n        return (\n            reverse(\n                \"authentik_api:certificatekeypair-view-certificate\",\n                kwargs={\"pk\": instance.pk},\n            )\n            + \"?download\"\n        )\n\n    def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:\n        \"\"\"Get URL to download private key\"\"\"\n        return (\n            reverse(\n                \"authentik_api:certificatekeypair-view-private-key\",\n                kwargs={\"pk\": instance.pk},\n            )\n            + \"?download\"\n        )\n\n    def validate_certificate_data(self, value: str) -> str:\n        \"\"\"Verify that input is a valid PEM x509 Certificate\"\"\"\n        try:\n            # Cast to string to fully load and parse certificate\n            # Prevents issues like https://github.com/goauthentik/authentik/issues/2082\n            str(load_pem_x509_certificate(value.encode(\"utf-8\"), default_backend()))\n        except ValueError as exc:\n            LOGGER.warning(\"Failed to load certificate\", exc=exc)\n            raise ValidationError(\"Unable to load certificate.\")\n        return value\n\n    def validate_key_data(self, value: str) -> str:\n        \"\"\"Verify that input is a valid PEM Key\"\"\"\n        # Since this field is optional, data can be empty.\n        if value != \"\":\n            try:\n                # Cast to string to fully load and parse certificate\n                # Prevents issues like https://github.com/goauthentik/authentik/issues/2082\n                str(\n                    load_pem_private_key(\n                        str.encode(\"\\n\".join([x.strip() for x in value.split(\"\\n\")])),\n                        password=None,\n                        backend=default_backend(),\n                    )\n                )\n            except (ValueError, TypeError) as exc:\n                LOGGER.warning(\"Failed to load private key\", exc=exc)\n                raise ValidationError(\"Unable to load private key (possibly encrypted?).\")\n        return value\n\n    class Meta:\n        model = CertificateKeyPair\n        fields = [\n            \"pk\",\n            \"name\",\n            \"fingerprint_sha256\",\n            \"fingerprint_sha1\",\n            \"certificate_data\",\n            \"key_data\",\n            \"cert_expiry\",\n            \"cert_subject\",\n            \"private_key_available\",\n            \"private_key_type\",\n            \"certificate_download_url\",\n            \"private_key_download_url\",\n            \"managed\",\n        ]\n        extra_kwargs = {\n            \"managed\": {\"read_only\": True},\n            \"key_data\": {\"write_only\": True},\n            \"certificate_data\": {\"write_only\": True},\n        }\n\n\nclass CertificateDataSerializer(PassiveSerializer):\n    \"\"\"Get CertificateKeyPair's data\"\"\"\n\n    data = CharField(read_only=True)\n\n\nclass CertificateGenerationSerializer(PassiveSerializer):\n    \"\"\"Certificate generation parameters\"\"\"\n\n    common_name = CharField()\n    subject_alt_name = CharField(required=False, allow_blank=True, label=_(\"Subject-alt name\"))\n    validity_days = IntegerField(initial=365)\n\n\nclass CertificateKeyPairFilter(FilterSet):\n    \"\"\"Filter for certificates\"\"\"\n\n    has_key = BooleanFilter(\n        label=\"Only return certificate-key pairs with keys\", method=\"filter_has_key\"\n    )\n\n    def filter_has_key(self, queryset, name, value):  # pragma: no cover\n        \"\"\"Only return certificate-key pairs with keys\"\"\"\n        return queryset.exclude(key_data__exact=\"\")\n\n    class Meta:\n        model = CertificateKeyPair\n        fields = [\"name\", \"managed\"]\n\n\nclass CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):\n    \"\"\"CertificateKeyPair Viewset\"\"\"\n\n    queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)\n    serializer_class = CertificateKeyPairSerializer\n    filterset_class = CertificateKeyPairFilter\n    ordering = [\"name\"]\n    search_fields = [\"name\"]\n    filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]\n\n    @extend_schema(\n        parameters=[\n            # Override the type for `has_key` above\n            OpenApiParameter(\n                \"has_key\",\n                bool,\n                required=False,\n                description=\"Only return certificate-key pairs with keys\",\n            ),\n            OpenApiParameter(\"include_details\", bool, default=True),\n        ]\n    )\n    def list(self, request, *args, **kwargs):\n        return super().list(request, *args, **kwargs)\n\n    @permission_required(None, [\"authentik_crypto.add_certificatekeypair\"])\n    @extend_schema(\n        request=CertificateGenerationSerializer(),\n        responses={\n            200: CertificateKeyPairSerializer,\n            400: OpenApiResponse(description=\"Bad request\"),\n        },\n    )\n    @action(detail=False, methods=[\"POST\"])\n    def generate(self, request: Request) -> Response:\n        \"\"\"Generate a new, self-signed certificate-key pair\"\"\"\n        data = CertificateGenerationSerializer(data=request.data)\n        if not data.is_valid():\n            return Response(data.errors, status=400)\n        raw_san = data.validated_data.get(\"subject_alt_name\", \"\")\n        sans = raw_san.split(\",\") if raw_san != \"\" else []\n        builder = CertificateBuilder(data.validated_data[\"common_name\"])\n        builder.build(\n            subject_alt_names=sans,\n            validity_days=int(data.validated_data[\"validity_days\"]),\n        )\n        instance = builder.save()\n        serializer = self.get_serializer(instance)\n        return Response(serializer.data)\n\n    @extend_schema(\n        parameters=[\n            OpenApiParameter(\n                name=\"download\",\n                location=OpenApiParameter.QUERY,\n                type=OpenApiTypes.BOOL,\n            )\n        ],\n        responses={200: CertificateDataSerializer(many=False)},\n    )\n    @action(detail=True, pagination_class=None, filter_backends=[])\n    def view_certificate(self, request: Request, pk: str) -> Response:\n        \"\"\"Return certificate-key pairs certificate and log access\"\"\"\n        certificate: CertificateKeyPair = self.get_object()\n        Event.new(  # noqa # nosec\n            EventAction.SECRET_VIEW,\n            secret=certificate,\n            type=\"certificate\",\n        ).from_http(request)\n        if \"download\" in request.query_params:\n            # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html\n            response = HttpResponse(\n                certificate.certificate_data, content_type=\"application/x-pem-file\"\n            )\n            response[\n                \"Content-Disposition\"\n            ] = f'attachment; filename=\"{certificate.name}_certificate.pem\"'\n            return response\n        return Response(CertificateDataSerializer({\"data\": certificate.certificate_data}).data)\n\n    @extend_schema(\n        parameters=[\n            OpenApiParameter(\n                name=\"download\",\n                location=OpenApiParameter.QUERY,\n                type=OpenApiTypes.BOOL,\n            )\n        ],\n        responses={200: CertificateDataSerializer(many=False)},\n    )\n    @action(detail=True, pagination_class=None, filter_backends=[])\n    def view_private_key(self, request: Request, pk: str) -> Response:\n        \"\"\"Return certificate-key pairs private key and log access\"\"\"\n        certificate: CertificateKeyPair = self.get_object()\n        Event.new(  # noqa # nosec\n            EventAction.SECRET_VIEW,\n            secret=certificate,\n            type=\"private_key\",\n        ).from_http(request)\n        if \"download\" in request.query_params:\n            # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html\n            response = HttpResponse(certificate.key_data, content_type=\"application/x-pem-file\")\n            response[\n                \"Content-Disposition\"\n            ] = f'attachment; filename=\"{certificate.name}_private_key.pem\"'\n            return response\n        return Response(CertificateDataSerializer({\"data\": certificate.key_data}).data)\n", "path": "authentik/crypto/api.py"}], "after_files": [{"content": "\"\"\"Crypto API Views\"\"\"\nfrom datetime import datetime\nfrom typing import Optional\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nfrom cryptography.x509 import load_pem_x509_certificate\nfrom django.http.response import HttpResponse\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\nfrom django_filters import FilterSet\nfrom django_filters.filters import BooleanFilter\nfrom drf_spectacular.types import OpenApiTypes\nfrom drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema\nfrom rest_framework.decorators import action\nfrom rest_framework.exceptions import ValidationError\nfrom rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField\nfrom rest_framework.filters import OrderingFilter, SearchFilter\nfrom rest_framework.request import Request\nfrom rest_framework.response import Response\nfrom rest_framework.serializers import ModelSerializer\nfrom rest_framework.viewsets import ModelViewSet\nfrom structlog.stdlib import get_logger\n\nfrom authentik.api.authorization import SecretKeyFilter\nfrom authentik.api.decorators import permission_required\nfrom authentik.core.api.used_by import UsedByMixin\nfrom authentik.core.api.utils import PassiveSerializer\nfrom authentik.crypto.apps import MANAGED_KEY\nfrom authentik.crypto.builder import CertificateBuilder\nfrom authentik.crypto.models import CertificateKeyPair\nfrom authentik.events.models import Event, EventAction\n\nLOGGER = get_logger()\n\n\nclass CertificateKeyPairSerializer(ModelSerializer):\n    \"\"\"CertificateKeyPair Serializer\"\"\"\n\n    fingerprint_sha256 = SerializerMethodField()\n    fingerprint_sha1 = SerializerMethodField()\n\n    cert_expiry = SerializerMethodField()\n    cert_subject = SerializerMethodField()\n    private_key_available = SerializerMethodField()\n    private_key_type = SerializerMethodField()\n\n    certificate_download_url = SerializerMethodField()\n    private_key_download_url = SerializerMethodField()\n\n    @property\n    def _should_include_details(self) -> bool:\n        request: Request = self.context.get(\"request\", None)\n        if not request:\n            return True\n        return str(request.query_params.get(\"include_details\", \"true\")).lower() == \"true\"\n\n    def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"Get certificate Hash (SHA256)\"\n        if not self._should_include_details:\n            return None\n        return instance.fingerprint_sha256\n\n    def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"Get certificate Hash (SHA1)\"\n        if not self._should_include_details:\n            return None\n        return instance.fingerprint_sha1\n\n    def get_cert_expiry(self, instance: CertificateKeyPair) -> Optional[datetime]:\n        \"Get certificate expiry\"\n        if not self._should_include_details:\n            return None\n        return DateTimeField().to_representation(instance.certificate.not_valid_after)\n\n    def get_cert_subject(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"\"\"Get certificate subject as full rfc4514\"\"\"\n        if not self._should_include_details:\n            return None\n        return instance.certificate.subject.rfc4514_string()\n\n    def get_private_key_available(self, instance: CertificateKeyPair) -> bool:\n        \"\"\"Show if this keypair has a private key configured or not\"\"\"\n        return instance.key_data != \"\" and instance.key_data is not None\n\n    def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]:\n        \"\"\"Get the private key's type, if set\"\"\"\n        if not self._should_include_details:\n            return None\n        key = instance.private_key\n        if key:\n            return key.__class__.__name__.replace(\"_\", \"\").lower().replace(\"privatekey\", \"\")\n        return None\n\n    def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:\n        \"\"\"Get URL to download certificate\"\"\"\n        return (\n            reverse(\n                \"authentik_api:certificatekeypair-view-certificate\",\n                kwargs={\"pk\": instance.pk},\n            )\n            + \"?download\"\n        )\n\n    def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:\n        \"\"\"Get URL to download private key\"\"\"\n        return (\n            reverse(\n                \"authentik_api:certificatekeypair-view-private-key\",\n                kwargs={\"pk\": instance.pk},\n            )\n            + \"?download\"\n        )\n\n    def validate_certificate_data(self, value: str) -> str:\n        \"\"\"Verify that input is a valid PEM x509 Certificate\"\"\"\n        try:\n            # Cast to string to fully load and parse certificate\n            # Prevents issues like https://github.com/goauthentik/authentik/issues/2082\n            str(load_pem_x509_certificate(value.encode(\"utf-8\"), default_backend()))\n        except ValueError as exc:\n            LOGGER.warning(\"Failed to load certificate\", exc=exc)\n            raise ValidationError(\"Unable to load certificate.\")\n        return value\n\n    def validate_key_data(self, value: str) -> str:\n        \"\"\"Verify that input is a valid PEM Key\"\"\"\n        # Since this field is optional, data can be empty.\n        if value != \"\":\n            try:\n                # Cast to string to fully load and parse certificate\n                # Prevents issues like https://github.com/goauthentik/authentik/issues/2082\n                str(\n                    load_pem_private_key(\n                        str.encode(\"\\n\".join([x.strip() for x in value.split(\"\\n\")])),\n                        password=None,\n                        backend=default_backend(),\n                    )\n                )\n            except (ValueError, TypeError) as exc:\n                LOGGER.warning(\"Failed to load private key\", exc=exc)\n                raise ValidationError(\"Unable to load private key (possibly encrypted?).\")\n        return value\n\n    class Meta:\n        model = CertificateKeyPair\n        fields = [\n            \"pk\",\n            \"name\",\n            \"fingerprint_sha256\",\n            \"fingerprint_sha1\",\n            \"certificate_data\",\n            \"key_data\",\n            \"cert_expiry\",\n            \"cert_subject\",\n            \"private_key_available\",\n            \"private_key_type\",\n            \"certificate_download_url\",\n            \"private_key_download_url\",\n            \"managed\",\n        ]\n        extra_kwargs = {\n            \"managed\": {\"read_only\": True},\n            \"key_data\": {\"write_only\": True},\n            \"certificate_data\": {\"write_only\": True},\n        }\n\n\nclass CertificateDataSerializer(PassiveSerializer):\n    \"\"\"Get CertificateKeyPair's data\"\"\"\n\n    data = CharField(read_only=True)\n\n\nclass CertificateGenerationSerializer(PassiveSerializer):\n    \"\"\"Certificate generation parameters\"\"\"\n\n    common_name = CharField()\n    subject_alt_name = CharField(required=False, allow_blank=True, label=_(\"Subject-alt name\"))\n    validity_days = IntegerField(initial=365)\n\n\nclass CertificateKeyPairFilter(FilterSet):\n    \"\"\"Filter for certificates\"\"\"\n\n    has_key = BooleanFilter(\n        label=\"Only return certificate-key pairs with keys\", method=\"filter_has_key\"\n    )\n\n    def filter_has_key(self, queryset, name, value):  # pragma: no cover\n        \"\"\"Only return certificate-key pairs with keys\"\"\"\n        if not value:\n            return queryset\n        return queryset.exclude(key_data__exact=\"\")\n\n    class Meta:\n        model = CertificateKeyPair\n        fields = [\"name\", \"managed\"]\n\n\nclass CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):\n    \"\"\"CertificateKeyPair Viewset\"\"\"\n\n    queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)\n    serializer_class = CertificateKeyPairSerializer\n    filterset_class = CertificateKeyPairFilter\n    ordering = [\"name\"]\n    search_fields = [\"name\"]\n    filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]\n\n    @extend_schema(\n        parameters=[\n            # Override the type for `has_key` above\n            OpenApiParameter(\n                \"has_key\",\n                bool,\n                required=False,\n                description=\"Only return certificate-key pairs with keys\",\n            ),\n            OpenApiParameter(\"include_details\", bool, default=True),\n        ]\n    )\n    def list(self, request, *args, **kwargs):\n        return super().list(request, *args, **kwargs)\n\n    @permission_required(None, [\"authentik_crypto.add_certificatekeypair\"])\n    @extend_schema(\n        request=CertificateGenerationSerializer(),\n        responses={\n            200: CertificateKeyPairSerializer,\n            400: OpenApiResponse(description=\"Bad request\"),\n        },\n    )\n    @action(detail=False, methods=[\"POST\"])\n    def generate(self, request: Request) -> Response:\n        \"\"\"Generate a new, self-signed certificate-key pair\"\"\"\n        data = CertificateGenerationSerializer(data=request.data)\n        if not data.is_valid():\n            return Response(data.errors, status=400)\n        raw_san = data.validated_data.get(\"subject_alt_name\", \"\")\n        sans = raw_san.split(\",\") if raw_san != \"\" else []\n        builder = CertificateBuilder(data.validated_data[\"common_name\"])\n        builder.build(\n            subject_alt_names=sans,\n            validity_days=int(data.validated_data[\"validity_days\"]),\n        )\n        instance = builder.save()\n        serializer = self.get_serializer(instance)\n        return Response(serializer.data)\n\n    @extend_schema(\n        parameters=[\n            OpenApiParameter(\n                name=\"download\",\n                location=OpenApiParameter.QUERY,\n                type=OpenApiTypes.BOOL,\n            )\n        ],\n        responses={200: CertificateDataSerializer(many=False)},\n    )\n    @action(detail=True, pagination_class=None, filter_backends=[])\n    def view_certificate(self, request: Request, pk: str) -> Response:\n        \"\"\"Return certificate-key pairs certificate and log access\"\"\"\n        certificate: CertificateKeyPair = self.get_object()\n        Event.new(  # noqa # nosec\n            EventAction.SECRET_VIEW,\n            secret=certificate,\n            type=\"certificate\",\n        ).from_http(request)\n        if \"download\" in request.query_params:\n            # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html\n            response = HttpResponse(\n                certificate.certificate_data, content_type=\"application/x-pem-file\"\n            )\n            response[\n                \"Content-Disposition\"\n            ] = f'attachment; filename=\"{certificate.name}_certificate.pem\"'\n            return response\n        return Response(CertificateDataSerializer({\"data\": certificate.certificate_data}).data)\n\n    @extend_schema(\n        parameters=[\n            OpenApiParameter(\n                name=\"download\",\n                location=OpenApiParameter.QUERY,\n                type=OpenApiTypes.BOOL,\n            )\n        ],\n        responses={200: CertificateDataSerializer(many=False)},\n    )\n    @action(detail=True, pagination_class=None, filter_backends=[])\n    def view_private_key(self, request: Request, pk: str) -> Response:\n        \"\"\"Return certificate-key pairs private key and log access\"\"\"\n        certificate: CertificateKeyPair = self.get_object()\n        Event.new(  # noqa # nosec\n            EventAction.SECRET_VIEW,\n            secret=certificate,\n            type=\"private_key\",\n        ).from_http(request)\n        if \"download\" in request.query_params:\n            # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html\n            response = HttpResponse(certificate.key_data, content_type=\"application/x-pem-file\")\n            response[\n                \"Content-Disposition\"\n            ] = f'attachment; filename=\"{certificate.name}_private_key.pem\"'\n            return response\n        return Response(CertificateDataSerializer({\"data\": certificate.key_data}).data)\n", "path": "authentik/crypto/api.py"}]}