diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index 8df905b0..220ad7ee 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -16,6 +16,8 @@ from lemur.users.schemas import UserNestedOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common import validators, missing +from lemur.common.fields import ArrowDateTime + class AuthorityInputSchema(LemurInputSchema): name = fields.String(required=True) @@ -23,8 +25,8 @@ class AuthorityInputSchema(LemurInputSchema): description = fields.String() common_name = fields.String(required=True, validate=validators.sensitive_domain) - validity_start = fields.DateTime() - validity_end = fields.DateTime() + validity_start = ArrowDateTime() + validity_end = ArrowDateTime() validity_years = fields.Integer() # certificate body fields diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 4741e494..7d0c4564 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -23,6 +23,8 @@ from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common import validators, missing from lemur.notifications import service as notification_service +from lemur.common.fields import ArrowDateTime + class CertificateSchema(LemurInputSchema): owner = fields.Email(required=True) @@ -46,8 +48,8 @@ class CertificateInputSchema(CertificateCreationSchema): common_name = fields.String(required=True, validate=validators.sensitive_domain) authority = fields.Nested(AssociatedAuthoritySchema, required=True) - validity_start = fields.DateTime() - validity_end = fields.DateTime() + validity_start = ArrowDateTime() + validity_end = ArrowDateTime() validity_years = fields.Integer() destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) diff --git a/lemur/common/fields.py b/lemur/common/fields.py new file mode 100644 index 00000000..3030cb0b --- /dev/null +++ b/lemur/common/fields.py @@ -0,0 +1,93 @@ +import arrow +import warnings +from datetime import datetime as dt +from marshmallow.fields import Field +from marshmallow import utils + + +class ArrowDateTime(Field): + """A formatted datetime string in UTC. + + Example: ``'2014-12-22T03:12:58.019077+00:00'`` + + Timezone-naive `datetime` objects are converted to + UTC (+00:00) by :meth:`Schema.dump `. + :meth:`Schema.load ` returns `datetime` + objects that are timezone-aware. + + :param str format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), + or a date format string. If `None`, defaults to "iso". + :param kwargs: The same keyword arguments that :class:`Field` receives. + + """ + + DATEFORMAT_SERIALIZATION_FUNCS = { + 'iso': utils.isoformat, + 'iso8601': utils.isoformat, + 'rfc': utils.rfcformat, + 'rfc822': utils.rfcformat, + } + + DATEFORMAT_DESERIALIZATION_FUNCS = { + 'iso': utils.from_iso, + 'iso8601': utils.from_iso, + 'rfc': utils.from_rfc, + 'rfc822': utils.from_rfc, + } + + DEFAULT_FORMAT = 'iso' + + localtime = False + default_error_messages = { + 'invalid': 'Not a valid datetime.', + 'format': '"{input}" cannot be formatted as a datetime.', + } + + def __init__(self, format=None, **kwargs): + super(ArrowDateTime, self).__init__(**kwargs) + # Allow this to be None. It may be set later in the ``_serialize`` + # or ``_desrialize`` methods This allows a Schema to dynamically set the + # dateformat, e.g. from a Meta option + self.dateformat = format + + def _add_to_schema(self, field_name, schema): + super(ArrowDateTime, self)._add_to_schema(field_name, schema) + self.dateformat = self.dateformat or schema.opts.dateformat + + def _serialize(self, value, attr, obj): + if value is None: + return None + self.dateformat = self.dateformat or self.DEFAULT_FORMAT + format_func = self.DATEFORMAT_SERIALIZATION_FUNCS.get(self.dateformat, None) + if format_func: + try: + return format_func(value, localtime=self.localtime) + except (AttributeError, ValueError) as err: + self.fail('format', input=value) + else: + return value.strftime(self.dateformat) + + def _deserialize(self, value, attr, data): + if not value: # Falsy values, e.g. '', None, [] are not valid + raise self.fail('invalid') + self.dateformat = self.dateformat or self.DEFAULT_FORMAT + func = self.DATEFORMAT_DESERIALIZATION_FUNCS.get(self.dateformat) + if func: + try: + return arrow.get(func(value)) + except (TypeError, AttributeError, ValueError): + raise self.fail('invalid') + elif self.dateformat: + try: + return dt.datetime.strptime(value, self.dateformat) + except (TypeError, AttributeError, ValueError): + raise self.fail('invalid') + elif utils.dateutil_available: + try: + return arrow.get(utils.from_datestring(value)) + except TypeError: + raise self.fail('invalid') + else: + warnings.warn('It is recommended that you install python-dateutil ' + 'for improved datetime deserialization.') + raise self.fail('invalid') diff --git a/lemur/common/missing.py b/lemur/common/missing.py index 2f9c5f89..a4bbba77 100644 --- a/lemur/common/missing.py +++ b/lemur/common/missing.py @@ -13,14 +13,12 @@ def convert_validity_years(data): """ if data.get('validity_years'): now = arrow.utcnow() - data['validity_start'] = now.date().isoformat() + data['validity_start'] = now.isoformat() end = now.replace(years=+int(data['validity_years'])) - data['validity_end'] = end.date().isoformat() - if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True): if is_weekend(end): end = end.replace(days=-2) - data['validity_end'] = end.date().isoformat() + data['validity_end'] = end.isoformat() return data diff --git a/lemur/common/validators.py b/lemur/common/validators.py index 439d4865..398a8509 100644 --- a/lemur/common/validators.py +++ b/lemur/common/validators.py @@ -109,10 +109,10 @@ def dates(data): raise ValidationError('Validity start must be before validity end.') if data.get('authority'): - if data.get('validity_start').replace(hour=0, minute=0, second=0, tzinfo=None) < data['authority'].authority_certificate.not_before.replace(hour=0, minute=0, second=0): + if data.get('validity_start').date() < data['authority'].authority_certificate.not_before.date(): raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before)) - if data.get('validity_end').replace(hour=0, minute=0, second=0, tzinfo=None) > data['authority'].authority_certificate.not_after.replace(hour=0, minute=0, second=0): + if data.get('validity_end').date() > data['authority'].authority_certificate.not_after.date(): raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after)) return data diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index 09fb5856..dfba412b 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -56,13 +56,12 @@ def determine_validity_years(end_date): :return: str validity in years """ now = arrow.utcnow() - then = arrow.get(end_date) - if then < now.replace(years=+1): + if end_date < now.replace(years=+1): return 1 - elif then < now.replace(years=+2): + elif end_date < now.replace(years=+2): return 2 - elif then < now.replace(years=+3): + elif end_date < now.replace(years=+3): return 3 raise Exception("DigiCert issued certificates cannot exceed three" @@ -75,9 +74,8 @@ def get_issuance(options): :param options: :return: """ - end_date = arrow.get(options['validity_end']) - validity_years = determine_validity_years(end_date) - return end_date, validity_years + validity_years = determine_validity_years(options['validity_end']) + return validity_years def process_options(options, csr): @@ -109,8 +107,8 @@ def process_options(options, csr): data['certificate']['dns_names'] = dns_names - end_date, validity_years = get_issuance(options) - data['custom_expiration_date'] = end_date.format('YYYY-MM-DD') + validity_years = get_issuance(options) + data['custom_expiration_date'] = options['validity_end'].format('YYYY-MM-DD') data['validity_years'] = validity_years return data diff --git a/lemur/plugins/lemur_digicert/tests/test_digicert.py b/lemur/plugins/lemur_digicert/tests/test_digicert.py index b3e7f68b..816f3a25 100644 --- a/lemur/plugins/lemur_digicert/tests/test_digicert.py +++ b/lemur/plugins/lemur_digicert/tests/test_digicert.py @@ -32,7 +32,7 @@ def test_process_options(app): 'dns_names': names, 'signature_hash': 'sha256' }, - 'organization': {'id': 0}, + 'organization': {'id': 111111}, 'validity_years': 1, 'custom_expiration_date': arrow.get(2017, 5, 7).format('YYYY-MM-DD') } @@ -47,18 +47,14 @@ def test_issuance(): 'validity_start': arrow.get(2016, 10, 30) } - end_date, period = get_issuance(options) - - assert period == 2 + assert get_issuance(options) == 2 options = { 'validity_end': arrow.get(2017, 5, 7), 'validity_start': arrow.get(2016, 10, 30) } - end_date, period = get_issuance(options) - - assert period == 1 + assert get_issuance(options) == 1 options = { 'validity_end': arrow.get(2020, 5, 7), @@ -66,7 +62,7 @@ def test_issuance(): } with pytest.raises(Exception): - end_date, period = get_issuance(options) + period = get_issuance(options) def test_signature_hash(app): diff --git a/lemur/plugins/lemur_verisign/plugin.py b/lemur/plugins/lemur_verisign/plugin.py index 2aa2690a..43dac676 100644 --- a/lemur/plugins/lemur_verisign/plugin.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -80,8 +80,8 @@ def process_options(options): } if options.get('validity_end'): - end_date, period = get_default_issuance(options) - data['specificEndDate'] = str(end_date) + period = get_default_issuance(options) + data['specificEndDate'] = options['validity_end'].format("MM/DD/YYYY") data['validityPeriod'] = period elif options.get('validity_years'): @@ -100,19 +100,16 @@ def get_default_issuance(options): :param options: :return: """ - specific_end_date = arrow.get(options['validity_end']).replace(days=-1).format("MM/DD/YYYY") - now = arrow.utcnow() - then = arrow.get(options['validity_end']) - if then < now.replace(years=+1): + if options['validity_end'] < now.replace(years=+1): validity_period = '1Y' - elif then < now.replace(years=+2): + elif options['validity_end'] < now.replace(years=+2): validity_period = '2Y' else: raise Exception("Verisign issued certificates cannot exceed two years in validity") - return specific_end_date, validity_period + return validity_period def handle_response(content): diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index e24d3689..f208dd03 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -72,7 +72,9 @@ LEMUR_INSTANCE_PROFILE = 'Lemur' DIGICERT_URL = 'https://www.digicert.com' DIGICERT_API_KEY = 'api-key' -DIGICERT_ORG_ID = 000000 +DIGICERT_ORG_ID = 111111 +DIGICERT_ROOT = "ROOT" +DIGICERT_INTERMEDIATE = "INTERMEDIATE" VERISIGN_URL = 'http://example.com' diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 493c7ca6..f38518c7 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals # at top of module import json import pytest import datetime +import arrow from freezegun import freeze_time @@ -133,6 +134,8 @@ def test_certificate_input_schema(client, authority): 'owner': 'jim@example.com', 'authority': {'id': authority.id}, 'description': 'testtestest', + 'validityEnd': arrow.get(2016, 11, 9).isoformat(), + 'validityStart': arrow.get(2015, 11, 9).isoformat() } data, errors = CertificateInputSchema().load(input_data) @@ -145,7 +148,7 @@ def test_certificate_input_schema(client, authority): assert data['country'] == 'US' assert data['location'] == 'Los Gatos' - assert len(data.keys()) == 13 + assert len(data.keys()) == 15 def test_certificate_input_with_extensions(client, authority): diff --git a/lemur/tests/test_missing.py b/lemur/tests/test_missing.py index 69981303..4f2c20c6 100644 --- a/lemur/tests/test_missing.py +++ b/lemur/tests/test_missing.py @@ -9,9 +9,9 @@ def test_convert_validity_years(session): with freeze_time("2016-01-01"): data = convert_validity_years(dict(validity_years=2)) - assert data['validity_start'] == arrow.utcnow().date().isoformat() - assert data['validity_end'] == arrow.utcnow().replace(years=+2).date().isoformat() + assert data['validity_start'] == arrow.utcnow().isoformat() + assert data['validity_end'] == arrow.utcnow().replace(years=+2).isoformat() with freeze_time("2015-01-10"): data = convert_validity_years(dict(validity_years=1)) - assert data['validity_end'] == arrow.utcnow().replace(years=+1, days=-2).date().isoformat() + assert data['validity_end'] == arrow.utcnow().replace(years=+1, days=-2).isoformat()