crypto: make certificate parsing optional for crypto api (#3711)
This commit is contained in:
parent
4f08a9424a
commit
44e4f2e561
|
@ -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(),
|
||||
|
|
|
@ -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)"""
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -87,6 +87,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
new CryptoApi(DEFAULT_CONFIG)
|
||||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((certs) => {
|
||||
return certs.results.map((cert) => {
|
||||
|
@ -122,6 +123,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
new CryptoApi(DEFAULT_CONFIG)
|
||||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((certs) => {
|
||||
return certs.results.map((cert) => {
|
||||
|
|
|
@ -182,6 +182,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
|
@ -189,6 +189,7 @@ ${this.instance?.redirectUris}</textarea
|
|||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
@ -200,7 +201,7 @@ ${this.instance?.redirectUris}</textarea
|
|||
value=${ifDefined(key.pk)}
|
||||
?selected=${selected}
|
||||
>
|
||||
${key.name} (${key.privateKeyType?.toUpperCase()})
|
||||
${key.name}
|
||||
</option>`;
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -346,6 +346,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
|
@ -158,6 +158,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
@ -196,6 +197,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
new CryptoApi(DEFAULT_CONFIG)
|
||||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
|
@ -160,6 +160,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
|||
new CryptoApi(DEFAULT_CONFIG)
|
||||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
|
@ -151,6 +151,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
new CryptoApi(DEFAULT_CONFIG)
|
||||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
|
@ -366,6 +366,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
.cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
hasKey: true,
|
||||
includeDetails: false,
|
||||
})
|
||||
.then((keys) => {
|
||||
return keys.results.map((key) => {
|
||||
|
|
Reference in a new issue