Cache parsed certificate instead of re-parsing for each field

Use @cached_property decorator to cache the results of parse_certificate().

This significantly cuts down on the number of times certs need to be
parsed for a list view.
This commit is contained in:
Marti Raudsepp 2018-06-19 18:41:12 +03:00 committed by Curtis Castrapel
parent 64132ba92b
commit b472e5e648
2 changed files with 35 additions and 21 deletions

View File

@ -21,6 +21,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils.types.arrow import ArrowType from sqlalchemy_utils.types.arrow import ArrowType
from werkzeug.utils import cached_property
import lemur.common.utils import lemur.common.utils
@ -142,7 +143,8 @@ class Certificate(db.Model):
sensitive_fields = ('private_key',) sensitive_fields = ('private_key',)
def __init__(self, **kwargs): def __init__(self, **kwargs):
cert = lemur.common.utils.parse_certificate(kwargs['body']) self.body = kwargs['body'].strip()
cert = self.parsed_cert
self.issuer = defaults.issuer(cert) self.issuer = defaults.issuer(cert)
self.cn = defaults.common_name(cert) self.cn = defaults.common_name(cert)
@ -159,7 +161,6 @@ class Certificate(db.Model):
defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san), self.serial) defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san), self.serial)
self.owner = kwargs['owner'] self.owner = kwargs['owner']
self.body = kwargs['body'].strip()
if kwargs.get('private_key'): if kwargs.get('private_key'):
self.private_key = kwargs['private_key'].strip() self.private_key = kwargs['private_key'].strip()
@ -184,40 +185,39 @@ class Certificate(db.Model):
for domain in defaults.domains(cert): for domain in defaults.domains(cert):
self.domains.append(Domain(name=domain)) self.domains.append(Domain(name=domain))
@cached_property
def parsed_cert(self):
assert self.body, "Certificate body not set"
return lemur.common.utils.parse_certificate(self.body)
@property @property
def active(self): def active(self):
return self.notify return self.notify
@property @property
def organization(self): def organization(self):
cert = lemur.common.utils.parse_certificate(self.body) return defaults.organization(self.parsed_cert)
return defaults.organization(cert)
@property @property
def organizational_unit(self): def organizational_unit(self):
cert = lemur.common.utils.parse_certificate(self.body) return defaults.organizational_unit(self.parsed_cert)
return defaults.organizational_unit(cert)
@property @property
def country(self): def country(self):
cert = lemur.common.utils.parse_certificate(self.body) return defaults.country(self.parsed_cert)
return defaults.country(cert)
@property @property
def state(self): def state(self):
cert = lemur.common.utils.parse_certificate(self.body) return defaults.state(self.parsed_cert)
return defaults.state(cert)
@property @property
def location(self): def location(self):
cert = lemur.common.utils.parse_certificate(self.body) return defaults.location(self.parsed_cert)
return defaults.location(cert)
@property @property
def key_type(self): def key_type(self):
cert = lemur.common.utils.parse_certificate(self.body) if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
if isinstance(cert.public_key(), rsa.RSAPublicKey): return 'RSA{key_size}'.format(key_size=self.parsed_cert.public_key().key_size)
return 'RSA{key_size}'.format(key_size=cert.public_key().key_size)
@property @property
def validity_remaining(self): def validity_remaining(self):
@ -229,13 +229,11 @@ class Certificate(db.Model):
@property @property
def subject(self): def subject(self):
cert = lemur.common.utils.parse_certificate(self.body) return self.parsed_cert.subject
return cert.subject
@property @property
def public_key(self): def public_key(self):
cert = lemur.common.utils.parse_certificate(self.body) return self.parsed_cert.public_key()
return cert.public_key()
@hybrid_property @hybrid_property
def expired(self): def expired(self):
@ -300,8 +298,7 @@ class Certificate(db.Model):
} }
try: try:
cert = lemur.common.utils.parse_certificate(self.body) for extension in self.parsed_cert.extensions:
for extension in cert.extensions:
value = extension.value value = extension.value
if isinstance(value, x509.BasicConstraints): if isinstance(value, x509.BasicConstraints):
return_extensions['basic_constraints'] = value return_extensions['basic_constraints'] = value

View File

@ -9,9 +9,11 @@ from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from marshmallow import ValidationError from marshmallow import ValidationError
from freezegun import freeze_time from freezegun import freeze_time
from mock import patch
from lemur.certificates.service import create_csr from lemur.certificates.service import create_csr
from lemur.certificates.views import * # noqa from lemur.certificates.views import * # noqa
from lemur.common import utils
from lemur.domains.models import Domain from lemur.domains.models import Domain
@ -66,6 +68,21 @@ def test_get_certificate_primitives(certificate):
assert len(primitives) == 24 assert len(primitives) == 24
def test_certificate_output_schema(session, certificate, issuer_plugin):
from lemur.certificates.schemas import CertificateOutputSchema
# Clear the cached attribute first
if 'parsed_cert' in certificate.__dict__:
del certificate.__dict__['parsed_cert']
# Make sure serialization parses the cert only once (uses cached 'parsed_cert' attribute)
with patch('lemur.common.utils.parse_certificate', side_effect=utils.parse_certificate) as wrapper:
data, errors = CertificateOutputSchema().dump(certificate)
assert data['issuer'] == 'Example'
assert wrapper.call_count == 1
def test_certificate_edit_schema(session): def test_certificate_edit_schema(session):
from lemur.certificates.schemas import CertificateEditInputSchema from lemur.certificates.schemas import CertificateEditInputSchema