diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index e71199fee..ff3391645 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -1,4 +1,5 @@ """Crypto API Views""" +from datetime import datetime from typing import Optional 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 rest_framework.decorators import action 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.response import Response from rest_framework.serializers import ModelSerializer @@ -34,7 +35,10 @@ LOGGER = get_logger() class CertificateKeyPairSerializer(ModelSerializer): """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() private_key_available = SerializerMethodField() private_key_type = SerializerMethodField() @@ -42,8 +46,35 @@ class CertificateKeyPairSerializer(ModelSerializer): certificate_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""" + if not self._should_include_details: + return None return instance.certificate.subject.rfc4514_string() 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]: """Get the private key's type, if set""" + if not self._should_include_details: + return None key = instance.private_key if key: return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "") @@ -171,6 +204,14 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): ordering = ["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"]) @extend_schema( request=CertificateGenerationSerializer(), diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py index 04ff429a3..3231f277b 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -1,5 +1,6 @@ """Crypto tests""" import datetime +from json import loads from os import makedirs from tempfile import TemporaryDirectory @@ -86,13 +87,35 @@ class TestCrypto(APITestCase): def test_list(self): """Test API List""" + 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}" ) 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): """Test certificate export (download)""" diff --git a/internal/outpost/ak/crypto.go b/internal/outpost/ak/crypto.go index 16042d7e1..d6d988e23 100644 --- a/internal/outpost/ak/crypto.go +++ b/internal/outpost/ak/crypto.go @@ -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") return "" } - return kp.FingerprintSha256 + return kp.GetFingerprintSha256() } func (cs *CryptoStore) Fetch(uuid string) error { diff --git a/schema.yml b/schema.yml index fece7d2fd..9e495ae95 100644 --- a/schema.yml +++ b/schema.yml @@ -4923,6 +4923,11 @@ paths: schema: type: boolean description: Only return certificate-key pairs with keys + - in: query + name: include_details + schema: + type: boolean + default: true - in: query name: managed schema: @@ -25924,16 +25929,20 @@ components: type: string fingerprint_sha256: type: string + nullable: true readOnly: true fingerprint_sha1: type: string + nullable: true readOnly: true cert_expiry: type: string format: date-time + nullable: true readOnly: true cert_subject: type: string + nullable: true readOnly: true private_key_available: type: boolean diff --git a/web/src/admin/outposts/ServiceConnectionDockerForm.ts b/web/src/admin/outposts/ServiceConnectionDockerForm.ts index 860339fe3..cfaf32237 100644 --- a/web/src/admin/outposts/ServiceConnectionDockerForm.ts +++ b/web/src/admin/outposts/ServiceConnectionDockerForm.ts @@ -87,6 +87,7 @@ export class ServiceConnectionDockerForm extends ModelForm { return certs.results.map((cert) => { @@ -122,6 +123,7 @@ export class ServiceConnectionDockerForm extends ModelForm { return certs.results.map((cert) => { diff --git a/web/src/admin/providers/ldap/LDAPProviderForm.ts b/web/src/admin/providers/ldap/LDAPProviderForm.ts index 106de1349..21ac5f7b9 100644 --- a/web/src/admin/providers/ldap/LDAPProviderForm.ts +++ b/web/src/admin/providers/ldap/LDAPProviderForm.ts @@ -182,6 +182,7 @@ export class LDAPProviderFormPage extends ModelForm { .cryptoCertificatekeypairsList({ ordering: "name", hasKey: true, + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index 92f2e5ea0..879be1798 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -189,6 +189,7 @@ ${this.instance?.redirectUris} { return keys.results.map((key) => { @@ -200,7 +201,7 @@ ${this.instance?.redirectUris} - ${key.name} (${key.privateKeyType?.toUpperCase()}) + ${key.name} `; }); }), diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index 71acf99aa..a4d6af167 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -346,6 +346,7 @@ export class ProxyProviderFormPage extends ModelForm { .cryptoCertificatekeypairsList({ ordering: "name", hasKey: true, + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index 5d34d3b3d..756b38835 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -158,6 +158,7 @@ export class SAMLProviderFormPage extends ModelForm { .cryptoCertificatekeypairsList({ ordering: "name", hasKey: true, + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { @@ -196,6 +197,7 @@ export class SAMLProviderFormPage extends ModelForm { new CryptoApi(DEFAULT_CONFIG) .cryptoCertificatekeypairsList({ ordering: "name", + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { diff --git a/web/src/admin/sources/ldap/LDAPSourceForm.ts b/web/src/admin/sources/ldap/LDAPSourceForm.ts index 5509ec339..30970b244 100644 --- a/web/src/admin/sources/ldap/LDAPSourceForm.ts +++ b/web/src/admin/sources/ldap/LDAPSourceForm.ts @@ -160,6 +160,7 @@ export class LDAPSourceForm extends ModelForm { new CryptoApi(DEFAULT_CONFIG) .cryptoCertificatekeypairsList({ ordering: "name", + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index 9f1ebfcb7..431a0cebf 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -151,6 +151,7 @@ export class SAMLSourceForm extends ModelForm { new CryptoApi(DEFAULT_CONFIG) .cryptoCertificatekeypairsList({ ordering: "name", + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => { diff --git a/web/src/admin/tenants/TenantForm.ts b/web/src/admin/tenants/TenantForm.ts index 1cfb5ac0b..f3aa0dead 100644 --- a/web/src/admin/tenants/TenantForm.ts +++ b/web/src/admin/tenants/TenantForm.ts @@ -366,6 +366,7 @@ export class TenantForm extends ModelForm { .cryptoCertificatekeypairsList({ ordering: "name", hasKey: true, + includeDetails: false, }) .then((keys) => { return keys.results.map((key) => {