From 24f29327776d5535040eb626ad369652c86784f6 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 11 May 2021 14:21:35 +0200 Subject: [PATCH] crypto: add ?download flag Signed-off-by: Jens Langhammer #861 --- authentik/crypto/api.py | 42 +++++++++++++++++++++++++++++-- authentik/crypto/tests.py | 44 +++++++++++++++++++++++++++++++++ authentik/providers/saml/api.py | 10 +++++++- swagger.yaml | 15 ++++++++--- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index fc10aa3a2..d07d6b722 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -3,7 +3,9 @@ import django_filters from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.x509 import load_pem_x509_certificate +from django.http.response import HttpResponse from django.utils.translation import gettext_lazy as _ +from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import action from rest_framework.fields import ( @@ -145,7 +147,16 @@ class CertificateKeyPairViewSet(ModelViewSet): serializer = self.get_serializer(instance) return Response(serializer.data) - @swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)}) + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + name="download", + in_=openapi.IN_QUERY, + type=openapi.TYPE_BOOLEAN, + ) + ], + responses={200: CertificateDataSerializer(many=False)}, + ) @action(detail=True, pagination_class=None, filter_backends=[]) # pylint: disable=invalid-name, unused-argument def view_certificate(self, request: Request, pk: str) -> Response: @@ -156,11 +167,29 @@ class CertificateKeyPairViewSet(ModelViewSet): secret=certificate, type="certificate", ).from_http(request) + if "download" in request._request.GET: + # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html + response = HttpResponse( + certificate.certificate_data, content_type="application/x-pem-file" + ) + response[ + "Content-Disposition" + ] = f'attachment; filename="{certificate.name}_certificate.pem"' + return response return Response( CertificateDataSerializer({"data": certificate.certificate_data}).data ) - @swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)}) + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + name="download", + in_=openapi.IN_QUERY, + type=openapi.TYPE_BOOLEAN, + ) + ], + responses={200: CertificateDataSerializer(many=False)}, + ) @action(detail=True, pagination_class=None, filter_backends=[]) # pylint: disable=invalid-name, unused-argument def view_private_key(self, request: Request, pk: str) -> Response: @@ -171,4 +200,13 @@ class CertificateKeyPairViewSet(ModelViewSet): secret=certificate, type="private_key", ).from_http(request) + if "download" in request._request.GET: + # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html + response = HttpResponse( + certificate.key_data, content_type="application/x-pem-file" + ) + response[ + "Content-Disposition" + ] = f'attachment; filename="{certificate.name}_private_key.pem"' + return response return Response(CertificateDataSerializer({"data": certificate.key_data}).data) diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py index 00dd0c30a..af9764078 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -2,7 +2,9 @@ import datetime from django.test import TestCase +from django.urls import reverse +from authentik.core.models import User from authentik.crypto.api import CertificateKeyPairSerializer from authentik.crypto.builder import CertificateBuilder from authentik.crypto.models import CertificateKeyPair @@ -47,3 +49,45 @@ class TestCrypto(TestCase): now = datetime.datetime.today() self.assertEqual(instance.name, "test-cert") self.assertEqual((instance.certificate.not_valid_after - now).days, 2) + + def test_certificate_download(self): + """Test certificate export (download)""" + self.client.force_login(User.objects.get(username="akadmin")) + keypair = CertificateKeyPair.objects.first() + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-certificate", + kwargs={"pk": keypair.pk}, + ) + ) + self.assertEqual(200, response.status_code) + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-certificate", + kwargs={"pk": keypair.pk}, + ) + + "?download", + ) + self.assertEqual(200, response.status_code) + self.assertIn("Content-Disposition", response) + + def test_private_key_download(self): + """Test private_key export (download)""" + self.client.force_login(User.objects.get(username="akadmin")) + keypair = CertificateKeyPair.objects.first() + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-private-key", + kwargs={"pk": keypair.pk}, + ) + ) + self.assertEqual(200, response.status_code) + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-private-key", + kwargs={"pk": keypair.pk}, + ) + + "?download", + ) + self.assertEqual(200, response.status_code) + self.assertIn("Content-Disposition", response) diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py index 6c7519fd6..84630d81d 100644 --- a/authentik/providers/saml/api.py +++ b/authentik/providers/saml/api.py @@ -5,6 +5,7 @@ from defusedxml.ElementTree import fromstring from django.http.response import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ +from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import action from rest_framework.fields import CharField, FileField, ReadOnlyField @@ -83,7 +84,14 @@ class SAMLProviderViewSet(ModelViewSet): responses={ 200: SAMLMetadataSerializer(many=False), 404: "Provider has no application assigned", - } + }, + manual_parameters=[ + openapi.Parameter( + name="download", + in_=openapi.IN_QUERY, + type=openapi.TYPE_BOOLEAN, + ) + ], ) @action(methods=["GET"], detail=True, permission_classes=[AllowAny]) # pylint: disable=invalid-name, unused-argument diff --git a/swagger.yaml b/swagger.yaml index 2b8da5f83..ff7727e20 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -2527,7 +2527,10 @@ paths: get: operationId: crypto_certificatekeypairs_view_certificate description: Return certificate-key pairs certificate and log access - parameters: [] + parameters: + - name: download + in: query + type: boolean responses: '200': description: '' @@ -2555,7 +2558,10 @@ paths: get: operationId: crypto_certificatekeypairs_view_private_key description: Return certificate-key pairs private key and log access - parameters: [] + parameters: + - name: download + in: query + type: boolean responses: '200': description: '' @@ -9696,7 +9702,10 @@ paths: get: operationId: providers_saml_metadata description: Return metadata as XML string - parameters: [] + parameters: + - name: download + in: query + type: boolean responses: '200': description: ''