crypto: add ?download flag

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#861
This commit is contained in:
Jens Langhammer 2021-05-11 14:21:35 +02:00
parent a6c6f22221
commit 24f2932777
4 changed files with 105 additions and 6 deletions

View File

@ -3,7 +3,9 @@ import django_filters
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate from cryptography.x509 import load_pem_x509_certificate
from django.http.response import HttpResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ( from rest_framework.fields import (
@ -145,7 +147,16 @@ class CertificateKeyPairViewSet(ModelViewSet):
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) 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=[]) @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument
def view_certificate(self, request: Request, pk: str) -> Response: def view_certificate(self, request: Request, pk: str) -> Response:
@ -156,11 +167,29 @@ class CertificateKeyPairViewSet(ModelViewSet):
secret=certificate, secret=certificate,
type="certificate", type="certificate",
).from_http(request) ).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( return Response(
CertificateDataSerializer({"data": certificate.certificate_data}).data 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=[]) @action(detail=True, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument
def view_private_key(self, request: Request, pk: str) -> Response: def view_private_key(self, request: Request, pk: str) -> Response:
@ -171,4 +200,13 @@ class CertificateKeyPairViewSet(ModelViewSet):
secret=certificate, secret=certificate,
type="private_key", type="private_key",
).from_http(request) ).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) return Response(CertificateDataSerializer({"data": certificate.key_data}).data)

View File

@ -2,7 +2,9 @@
import datetime import datetime
from django.test import TestCase 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.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
@ -47,3 +49,45 @@ class TestCrypto(TestCase):
now = datetime.datetime.today() now = datetime.datetime.today()
self.assertEqual(instance.name, "test-cert") self.assertEqual(instance.name, "test-cert")
self.assertEqual((instance.certificate.not_valid_after - now).days, 2) 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)

View File

@ -5,6 +5,7 @@ from defusedxml.ElementTree import fromstring
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, ReadOnlyField from rest_framework.fields import CharField, FileField, ReadOnlyField
@ -83,7 +84,14 @@ class SAMLProviderViewSet(ModelViewSet):
responses={ responses={
200: SAMLMetadataSerializer(many=False), 200: SAMLMetadataSerializer(many=False),
404: "Provider has no application assigned", 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]) @action(methods=["GET"], detail=True, permission_classes=[AllowAny])
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument

View File

@ -2527,7 +2527,10 @@ paths:
get: get:
operationId: crypto_certificatekeypairs_view_certificate operationId: crypto_certificatekeypairs_view_certificate
description: Return certificate-key pairs certificate and log access description: Return certificate-key pairs certificate and log access
parameters: [] parameters:
- name: download
in: query
type: boolean
responses: responses:
'200': '200':
description: '' description: ''
@ -2555,7 +2558,10 @@ paths:
get: get:
operationId: crypto_certificatekeypairs_view_private_key operationId: crypto_certificatekeypairs_view_private_key
description: Return certificate-key pairs private key and log access description: Return certificate-key pairs private key and log access
parameters: [] parameters:
- name: download
in: query
type: boolean
responses: responses:
'200': '200':
description: '' description: ''
@ -9696,7 +9702,10 @@ paths:
get: get:
operationId: providers_saml_metadata operationId: providers_saml_metadata
description: Return metadata as XML string description: Return metadata as XML string
parameters: [] parameters:
- name: download
in: query
type: boolean
responses: responses:
'200': '200':
description: '' description: ''