crypto: make certificate parsing optional for crypto api (#3711)
This commit is contained in:
		| @ -1,4 +1,5 @@ | |||||||
| """Crypto API Views""" | """Crypto API Views""" | ||||||
|  | from datetime import datetime | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| from cryptography.hazmat.backends import default_backend | from cryptography.hazmat.backends import default_backend | ||||||
| @ -13,7 +14,7 @@ from drf_spectacular.types import OpenApiTypes | |||||||
| from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField | from rest_framework.fields import CharField, IntegerField, SerializerMethodField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| @ -34,7 +35,10 @@ LOGGER = get_logger() | |||||||
| class CertificateKeyPairSerializer(ModelSerializer): | class CertificateKeyPairSerializer(ModelSerializer): | ||||||
|     """CertificateKeyPair Serializer""" |     """CertificateKeyPair Serializer""" | ||||||
|  |  | ||||||
|     cert_expiry = DateTimeField(source="certificate.not_valid_after", read_only=True) |     fingerprint_sha256 = SerializerMethodField() | ||||||
|  |     fingerprint_sha1 = SerializerMethodField() | ||||||
|  |  | ||||||
|  |     cert_expiry = SerializerMethodField() | ||||||
|     cert_subject = SerializerMethodField() |     cert_subject = SerializerMethodField() | ||||||
|     private_key_available = SerializerMethodField() |     private_key_available = SerializerMethodField() | ||||||
|     private_key_type = SerializerMethodField() |     private_key_type = SerializerMethodField() | ||||||
| @ -42,8 +46,35 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|     certificate_download_url = SerializerMethodField() |     certificate_download_url = SerializerMethodField() | ||||||
|     private_key_download_url = SerializerMethodField() |     private_key_download_url = SerializerMethodField() | ||||||
|  |  | ||||||
|     def get_cert_subject(self, instance: CertificateKeyPair) -> str: |     @property | ||||||
|  |     def _should_include_details(self) -> bool: | ||||||
|  |         request: Request = self.context.get("request", None) | ||||||
|  |         if not request: | ||||||
|  |             return True | ||||||
|  |         return str(request.query_params.get("include_details", "true")).lower() == "true" | ||||||
|  |  | ||||||
|  |     def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> Optional[str]: | ||||||
|  |         "Get certificate Hash (SHA256)" | ||||||
|  |         if not self._should_include_details: | ||||||
|  |             return None | ||||||
|  |         return instance.fingerprint_sha256 | ||||||
|  |  | ||||||
|  |     def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> Optional[str]: | ||||||
|  |         "Get certificate Hash (SHA1)" | ||||||
|  |         if not self._should_include_details: | ||||||
|  |             return None | ||||||
|  |         return instance.fingerprint_sha1 | ||||||
|  |  | ||||||
|  |     def get_cert_expiry(self, instance: CertificateKeyPair) -> Optional[datetime]: | ||||||
|  |         "Get certificate expiry" | ||||||
|  |         if not self._should_include_details: | ||||||
|  |             return None | ||||||
|  |         return instance.certificate.not_valid_after | ||||||
|  |  | ||||||
|  |     def get_cert_subject(self, instance: CertificateKeyPair) -> Optional[str]: | ||||||
|         """Get certificate subject as full rfc4514""" |         """Get certificate subject as full rfc4514""" | ||||||
|  |         if not self._should_include_details: | ||||||
|  |             return None | ||||||
|         return instance.certificate.subject.rfc4514_string() |         return instance.certificate.subject.rfc4514_string() | ||||||
|  |  | ||||||
|     def get_private_key_available(self, instance: CertificateKeyPair) -> bool: |     def get_private_key_available(self, instance: CertificateKeyPair) -> bool: | ||||||
| @ -52,6 +83,8 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]: |     def get_private_key_type(self, instance: CertificateKeyPair) -> Optional[str]: | ||||||
|         """Get the private key's type, if set""" |         """Get the private key's type, if set""" | ||||||
|  |         if not self._should_include_details: | ||||||
|  |             return None | ||||||
|         key = instance.private_key |         key = instance.private_key | ||||||
|         if key: |         if key: | ||||||
|             return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "") |             return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "") | ||||||
| @ -171,6 +204,14 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): | |||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|     search_fields = ["name"] |     search_fields = ["name"] | ||||||
|  |  | ||||||
|  |     @extend_schema( | ||||||
|  |         parameters=[ | ||||||
|  |             OpenApiParameter("include_details", bool, default=True), | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |     def list(self, request, *args, **kwargs): | ||||||
|  |         return super().list(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     @permission_required(None, ["authentik_crypto.add_certificatekeypair"]) |     @permission_required(None, ["authentik_crypto.add_certificatekeypair"]) | ||||||
|     @extend_schema( |     @extend_schema( | ||||||
|         request=CertificateGenerationSerializer(), |         request=CertificateGenerationSerializer(), | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| """Crypto tests""" | """Crypto tests""" | ||||||
| import datetime | import datetime | ||||||
|  | from json import loads | ||||||
| from os import makedirs | from os import makedirs | ||||||
| from tempfile import TemporaryDirectory | from tempfile import TemporaryDirectory | ||||||
|  |  | ||||||
| @ -86,13 +87,35 @@ class TestCrypto(APITestCase): | |||||||
|  |  | ||||||
|     def test_list(self): |     def test_list(self): | ||||||
|         """Test API List""" |         """Test API List""" | ||||||
|  |         cert = create_test_cert() | ||||||
|         self.client.force_login(create_test_admin_user()) |         self.client.force_login(create_test_admin_user()) | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_api:certificatekeypair-list", |                 "authentik_api:certificatekeypair-list", | ||||||
|             ) |             ) | ||||||
|  |             + f"?name={cert.name}" | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(200, response.status_code) |         self.assertEqual(200, response.status_code) | ||||||
|  |         body = loads(response.content.decode()) | ||||||
|  |         api_cert = [x for x in body["results"] if x["name"] == cert.name][0] | ||||||
|  |         self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1) | ||||||
|  |         self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256) | ||||||
|  |  | ||||||
|  |     def test_list_without_details(self): | ||||||
|  |         """Test API List (no details)""" | ||||||
|  |         cert = create_test_cert() | ||||||
|  |         self.client.force_login(create_test_admin_user()) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_api:certificatekeypair-list", | ||||||
|  |             ) | ||||||
|  |             + f"?name={cert.name}&include_details=false" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(200, response.status_code) | ||||||
|  |         body = loads(response.content.decode()) | ||||||
|  |         api_cert = [x for x in body["results"] if x["name"] == cert.name][0] | ||||||
|  |         self.assertEqual(api_cert["fingerprint_sha1"], None) | ||||||
|  |         self.assertEqual(api_cert["fingerprint_sha256"], None) | ||||||
|  |  | ||||||
|     def test_certificate_download(self): |     def test_certificate_download(self): | ||||||
|         """Test certificate export (download)""" |         """Test certificate export (download)""" | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ func (cs *CryptoStore) getFingerprint(uuid string) string { | |||||||
| 		cs.log.WithField("uuid", uuid).WithError(err).Warning("Failed to fetch certificate's fingerprint") | 		cs.log.WithField("uuid", uuid).WithError(err).Warning("Failed to fetch certificate's fingerprint") | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
| 	return kp.FingerprintSha256 | 	return kp.GetFingerprintSha256() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (cs *CryptoStore) Fetch(uuid string) error { | func (cs *CryptoStore) Fetch(uuid string) error { | ||||||
|  | |||||||
| @ -4923,6 +4923,11 @@ paths: | |||||||
|         schema: |         schema: | ||||||
|           type: boolean |           type: boolean | ||||||
|         description: Only return certificate-key pairs with keys |         description: Only return certificate-key pairs with keys | ||||||
|  |       - in: query | ||||||
|  |         name: include_details | ||||||
|  |         schema: | ||||||
|  |           type: boolean | ||||||
|  |           default: true | ||||||
|       - in: query |       - in: query | ||||||
|         name: managed |         name: managed | ||||||
|         schema: |         schema: | ||||||
| @ -25924,16 +25929,20 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         fingerprint_sha256: |         fingerprint_sha256: | ||||||
|           type: string |           type: string | ||||||
|  |           nullable: true | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         fingerprint_sha1: |         fingerprint_sha1: | ||||||
|           type: string |           type: string | ||||||
|  |           nullable: true | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         cert_expiry: |         cert_expiry: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|  |           nullable: true | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         cert_subject: |         cert_subject: | ||||||
|           type: string |           type: string | ||||||
|  |           nullable: true | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         private_key_available: |         private_key_available: | ||||||
|           type: boolean |           type: boolean | ||||||
|  | |||||||
| @ -87,6 +87,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti | |||||||
|                         new CryptoApi(DEFAULT_CONFIG) |                         new CryptoApi(DEFAULT_CONFIG) | ||||||
|                             .cryptoCertificatekeypairsList({ |                             .cryptoCertificatekeypairsList({ | ||||||
|                                 ordering: "name", |                                 ordering: "name", | ||||||
|  |                                 includeDetails: false, | ||||||
|                             }) |                             }) | ||||||
|                             .then((certs) => { |                             .then((certs) => { | ||||||
|                                 return certs.results.map((cert) => { |                                 return certs.results.map((cert) => { | ||||||
| @ -122,6 +123,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti | |||||||
|                         new CryptoApi(DEFAULT_CONFIG) |                         new CryptoApi(DEFAULT_CONFIG) | ||||||
|                             .cryptoCertificatekeypairsList({ |                             .cryptoCertificatekeypairsList({ | ||||||
|                                 ordering: "name", |                                 ordering: "name", | ||||||
|  |                                 includeDetails: false, | ||||||
|                             }) |                             }) | ||||||
|                             .then((certs) => { |                             .then((certs) => { | ||||||
|                                 return certs.results.map((cert) => { |                                 return certs.results.map((cert) => { | ||||||
|  | |||||||
| @ -182,6 +182,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> { | |||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|                                         hasKey: true, |                                         hasKey: true, | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
| @ -189,6 +189,7 @@ ${this.instance?.redirectUris}</textarea | |||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|                                         hasKey: true, |                                         hasKey: true, | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
| @ -200,7 +201,7 @@ ${this.instance?.redirectUris}</textarea | |||||||
|                                                 value=${ifDefined(key.pk)} |                                                 value=${ifDefined(key.pk)} | ||||||
|                                                 ?selected=${selected} |                                                 ?selected=${selected} | ||||||
|                                             > |                                             > | ||||||
|                                                 ${key.name} (${key.privateKeyType?.toUpperCase()}) |                                                 ${key.name} | ||||||
|                                             </option>`; |                                             </option>`; | ||||||
|                                         }); |                                         }); | ||||||
|                                     }), |                                     }), | ||||||
|  | |||||||
| @ -346,6 +346,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> { | |||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|                                         hasKey: true, |                                         hasKey: true, | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
| @ -158,6 +158,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> { | |||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|                                         hasKey: true, |                                         hasKey: true, | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
| @ -196,6 +197,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> { | |||||||
|                                 new CryptoApi(DEFAULT_CONFIG) |                                 new CryptoApi(DEFAULT_CONFIG) | ||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
| @ -160,6 +160,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> { | |||||||
|                                 new CryptoApi(DEFAULT_CONFIG) |                                 new CryptoApi(DEFAULT_CONFIG) | ||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
| @ -151,6 +151,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> { | |||||||
|                                 new CryptoApi(DEFAULT_CONFIG) |                                 new CryptoApi(DEFAULT_CONFIG) | ||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
| @ -366,6 +366,7 @@ export class TenantForm extends ModelForm<Tenant, string> { | |||||||
|                                     .cryptoCertificatekeypairsList({ |                                     .cryptoCertificatekeypairsList({ | ||||||
|                                         ordering: "name", |                                         ordering: "name", | ||||||
|                                         hasKey: true, |                                         hasKey: true, | ||||||
|  |                                         includeDetails: false, | ||||||
|                                     }) |                                     }) | ||||||
|                                     .then((keys) => { |                                     .then((keys) => { | ||||||
|                                         return keys.results.map((key) => { |                                         return keys.results.map((key) => { | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L