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"""
|
"""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 a new issue