From dcb18a57c4eee85d43d94332ea0072c007baf3ed Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sat, 15 Oct 2016 00:04:35 -0700 Subject: [PATCH] Adds option to restrict certificate expiration dates to weekdays. (#453) * Adding ability to restrict certificate creation to weekdays. * Ensuring that we test for weekends. --- docs/administration.rst | 7 ++++- lemur/authorities/schemas.py | 2 +- lemur/certificates/schemas.py | 2 +- lemur/common/missing.py | 31 +++++++++++++++------ lemur/common/utils.py | 11 ++++++++ lemur/common/validators.py | 25 ++++++----------- lemur/tests/conf.py | 2 ++ lemur/tests/test_certificates.py | 2 +- lemur/tests/test_missing.py | 17 ++++++++++++ lemur/tests/test_validators.py | 47 +++++++++++++++++--------------- setup.py | 3 +- 11 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 lemur/tests/test_missing.py diff --git a/docs/administration.rst b/docs/administration.rst index 8bbe8f46..60952152 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -51,7 +51,7 @@ Basic Configuration CORS = False -.. data:: SQLACHEMY_DATABASE_URI +.. data:: SQLALCHEMY_DATABASE_URI :noindex: If you have ever used sqlalchemy before this is the standard connection string used. Lemur uses a postgres database and the connection string would look something like: @@ -61,6 +61,11 @@ Basic Configuration SQLALCHEMY_DATABASE_URI = 'postgresql://:@:5432/lemur' +.. data:: LEMUR_ALLOW_WEEKEND_EXPIRATION + :noindex: + + Specifies whether to allow certificates created by Lemur to expire on weekends. Default is True. + .. data:: LEMUR_RESTRICTED_DOMAINS :noindex: diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index 2ba3d085..8df905b0 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -62,7 +62,7 @@ class AuthorityInputSchema(LemurInputSchema): @pre_load def ensure_dates(self, data): - return missing.dates(data) + return missing.convert_validity_years(data) class AuthorityUpdateSchema(LemurInputSchema): diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index c4ce7f28..5c2f493c 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -72,7 +72,7 @@ class CertificateInputSchema(CertificateCreationSchema): @pre_load def ensure_dates(self, data): - return missing.dates(data) + return missing.convert_validity_years(data) class CertificateEditInputSchema(CertificateSchema): diff --git a/lemur/common/missing.py b/lemur/common/missing.py index 89cb91dc..2f9c5f89 100644 --- a/lemur/common/missing.py +++ b/lemur/common/missing.py @@ -1,13 +1,26 @@ import arrow +from flask import current_app + +from lemur.common.utils import is_weekend -def dates(data): - # ensure that validity_start and validity_end are always set - if not(data.get('validity_start') and data.get('validity_end')): - if data.get('validity_years'): - num_years = data['validity_years'] - now = arrow.utcnow() - then = now.replace(years=+int(num_years)) +def convert_validity_years(data): + """ + Convert validity years to validity_start and validity_end - data['validity_start'] = now.isoformat() - data['validity_end'] = then.isoformat() + :param data: + :return: + """ + if data.get('validity_years'): + now = arrow.utcnow() + data['validity_start'] = now.date().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() + + return data diff --git a/lemur/common/utils.py b/lemur/common/utils.py index dae5e1f9..ec21cc7e 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -41,3 +41,14 @@ def parse_certificate(body): return x509.load_pem_x509_certificate(body, default_backend()) return x509.load_pem_x509_certificate(bytes(body, 'utf8'), default_backend()) return x509.load_pem_x509_certificate(body.encode('utf-8'), default_backend()) + + +def is_weekend(date): + """ + Determines if a given date is on a weekend. + + :param date: + :return: + """ + if date.weekday() > 5: + return True diff --git a/lemur/common/validators.py b/lemur/common/validators.py index 7c216402..f5468e05 100644 --- a/lemur/common/validators.py +++ b/lemur/common/validators.py @@ -1,16 +1,14 @@ - -import arrow import re -from flask import current_app -from marshmallow.exceptions import ValidationError +from flask import current_app from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization +from marshmallow.exceptions import ValidationError -from lemur.common.utils import parse_certificate -from lemur.domains import service as domain_service from lemur.auth.permissions import SensitiveDomainPermission +from lemur.common.utils import parse_certificate, is_weekend +from lemur.domains import service as domain_service def public_certificate(body): @@ -102,6 +100,10 @@ def dates(data): raise ValidationError('If validity end is specified so must validity start.') if data.get('validity_start') and data.get('validity_end'): + if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True): + if is_weekend(data.get('validity_end')): + raise ValidationError('Validity end must not land on a weekend.') + if not data['validity_start'] < data['validity_end']: raise ValidationError('Validity start must be before validity end.') @@ -112,13 +114,4 @@ def dates(data): 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): raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after)) - if data.get('validity_years'): - now = arrow.utcnow() - end = now.replace(years=+data['validity_years']) - - if data.get('authority'): - if now.naive < data['authority'].authority_certificate.not_before: - raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before)) - - if end.naive > data['authority'].authority_certificate.not_after: - raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after)) + return data diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index c4c5bbd9..8e94ff91 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -46,6 +46,8 @@ LEMUR_DEFAULT_LOCATION = 'Los Gatos' LEMUR_DEFAULT_ORGANIZATION = 'Example, Inc.' LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = 'Example' +LEMUR_ALLOW_WEEKEND_EXPIRATION = False + # Database # modify this if you are not using a local database diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 87e05282..94974176 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -5,7 +5,7 @@ import json from lemur.certificates.views import * # noqa -from .vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \ +from lemur.tests.vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \ INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR diff --git a/lemur/tests/test_missing.py b/lemur/tests/test_missing.py new file mode 100644 index 00000000..69981303 --- /dev/null +++ b/lemur/tests/test_missing.py @@ -0,0 +1,17 @@ +import arrow + +from freezegun import freeze_time + + +def test_convert_validity_years(session): + from lemur.common.missing import convert_validity_years + + 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() + + 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() diff --git a/lemur/tests/test_validators.py b/lemur/tests/test_validators.py index 3dbdaa8f..5e31ac5d 100644 --- a/lemur/tests/test_validators.py +++ b/lemur/tests/test_validators.py @@ -1,29 +1,32 @@ -from marshmallow.exceptions import ValidationError +import pytest +from datetime import datetime from .vectors import PRIVATE_KEY_STR +from marshmallow.exceptions import ValidationError -def test_private_key(): +def test_private_key(session): from lemur.common.validators import private_key - try: - private_key(PRIVATE_KEY_STR) - assert True - except ValidationError: - assert False, "failed to validate private key as a bytes object" + private_key(PRIVATE_KEY_STR) + private_key(PRIVATE_KEY_STR.decode('utf-8')) -def test_private_key_str_object(): - from lemur.common.validators import private_key - try: - private_key(PRIVATE_KEY_STR.decode('utf-8')) - assert True - except ValidationError: - assert False, "failed to validate private key as a str object" - - -def test_private_key_invalid(): - from lemur.common.validators import private_key - try: + with pytest.raises(ValidationError): private_key('invalid_private_key') - assert False, "invalid private key should have raised an exception" - except ValidationError: - assert True + + +def test_dates(session): + from lemur.common.validators import dates + + dates(dict(validity_start=datetime(2016, 1, 1), validity_end=datetime(2016, 1, 5))) + + with pytest.raises(ValidationError): + dates(dict(validity_start=datetime(2016, 1, 1))) + + with pytest.raises(ValidationError): + dates(dict(validity_end=datetime(2016, 1, 1))) + + with pytest.raises(ValidationError): + dates(dict(validity_start=datetime(2016, 1, 5), validity_end=datetime(2016, 1, 1))) + + with pytest.raises(ValidationError): + dates(dict(validity_start=datetime(2016, 1, 1), validity_end=datetime(2016, 1, 10))) diff --git a/setup.py b/setup.py index 031e302c..865531be 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,8 @@ install_requires = [ 'boto3==1.3.0', 'acme==0.1.0', 'retrying==1.3.3', - 'tabulate==0.7.5' + 'tabulate==0.7.5', + 'freezegun==0.3.7', ] tests_require = [