crypto: make certificate parsing optional for crypto api (#3711)

This commit is contained in:
Jens L 2022-10-01 00:06:00 +02:00 committed by GitHub
parent 4f08a9424a
commit 44e4f2e561
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 88 additions and 5 deletions

View file

@ -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(),

View file

@ -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)"""

View file

@ -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 {

View file

@ -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

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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>`;
});
}),

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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) => {