Merge branch 'master' of github.com:jtschladen/lemur into sns

This commit is contained in:
Jasmine Schladen 2020-10-19 16:29:33 -07:00
commit 669a4273c2
12 changed files with 106 additions and 14 deletions

View File

@ -6,6 +6,8 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import json
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
@ -80,5 +82,21 @@ class Authority(db.Model):
def plugin(self): def plugin(self):
return plugins.get(self.plugin_name) return plugins.get(self.plugin_name)
@property
def is_cab_compliant(self):
"""
Parse the options to find whether authority is CAB Forum Compliant,
i.e., adhering to the CA/Browser Forum Baseline Requirements.
Returns None if option is not available
"""
if not self.options:
return None
for option in json.loads(self.options):
if "name" in option and option["name"] == 'cab_compliant':
return option["value"]
return None
def __repr__(self): def __repr__(self):
return "Authority(name={name})".format(name=self.name) return "Authority(name={name})".format(name=self.name)

View File

@ -139,6 +139,7 @@ class AuthorityNestedOutputSchema(LemurOutputSchema):
plugin = fields.Nested(PluginOutputSchema) plugin = fields.Nested(PluginOutputSchema)
active = fields.Boolean() active = fields.Boolean()
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema, only=["max_issuance_days", "default_validity_days"]) authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema, only=["max_issuance_days", "default_validity_days"])
is_cab_compliant = fields.Boolean()
authority_update_schema = AuthorityUpdateSchema() authority_update_schema = AuthorityUpdateSchema()

View File

@ -8,7 +8,7 @@
from flask import current_app from flask import current_app
from flask_restful import inputs from flask_restful import inputs
from flask_restful.reqparse import RequestParser from flask_restful.reqparse import RequestParser
from marshmallow import fields, validate, validates_schema, post_load, pre_load from marshmallow import fields, validate, validates_schema, post_load, pre_load, post_dump
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
@ -332,6 +332,27 @@ class CertificateOutputSchema(LemurOutputSchema):
) )
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema) rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
country = fields.String()
location = fields.String()
state = fields.String()
organization = fields.String()
organizational_unit = fields.String()
@post_dump
def handle_subject_details(self, data):
# Remove subject details if authority is CA/Browser Forum compliant. The code will use default set of values in that case.
# If CA/Browser Forum compliance of an authority is unknown (None), it is safe to fallback to default values. Thus below
# condition checks for 'not False' ==> 'True or None'
if data.get("authority"):
is_cab_compliant = data.get("authority").get("isCabCompliant")
if is_cab_compliant is not False:
data.pop("country", None)
data.pop("state", None)
data.pop("location", None)
data.pop("organization", None)
data.pop("organizational_unit", None)
class CertificateShortOutputSchema(LemurOutputSchema): class CertificateShortOutputSchema(LemurOutputSchema):
id = fields.Integer() id = fields.Integer()

View File

@ -20,8 +20,9 @@ fileConfig(config.config_file_name)
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
from flask import current_app from flask import current_app
db_url_escaped = current_app.config.get('SQLALCHEMY_DATABASE_URI').replace('%', '%%')
config.set_main_option( config.set_main_option(
"sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") "sqlalchemy.url", db_url_escaped
) )
target_metadata = current_app.extensions["migrate"].db.metadata target_metadata = current_app.extensions["migrate"].db.metadata

View File

@ -175,7 +175,6 @@ def map_cis_fields(options, csr):
}, },
"organization": { "organization": {
"name": options["organization"], "name": options["organization"],
"units": [options["organizational_unit"]],
}, },
} }
# possibility to default to a SIGNING_ALGORITHM for a given profile # possibility to default to a SIGNING_ALGORITHM for a given profile

View File

@ -121,7 +121,7 @@ def test_map_cis_fields_with_validity_years(mock_current_app, authority):
"csr": CSR_STR, "csr": CSR_STR,
"additional_dns_names": names, "additional_dns_names": names,
"signature_hash": "sha256", "signature_hash": "sha256",
"organization": {"name": "Example, Inc.", "units": ["Example Org"]}, "organization": {"name": "Example, Inc."},
"validity": { "validity": {
"valid_to": arrow.get(2018, 11, 3).format("YYYY-MM-DDTHH:MM") + "Z" "valid_to": arrow.get(2018, 11, 3).format("YYYY-MM-DDTHH:MM") + "Z"
}, },
@ -157,7 +157,7 @@ def test_map_cis_fields_with_validity_end_and_start(mock_current_app, app, autho
"csr": CSR_STR, "csr": CSR_STR,
"additional_dns_names": names, "additional_dns_names": names,
"signature_hash": "sha256", "signature_hash": "sha256",
"organization": {"name": "Example, Inc.", "units": ["Example Org"]}, "organization": {"name": "Example, Inc."},
"validity": { "validity": {
"valid_to": arrow.get(2017, 5, 7).format("YYYY-MM-DDTHH:MM") + "Z" "valid_to": arrow.get(2017, 5, 7).format("YYYY-MM-DDTHH:MM") + "Z"
}, },

View File

@ -255,9 +255,6 @@ angular.module('lemur')
$scope.certificate.replacedBy = []; // should not clone 'replaced by' info $scope.certificate.replacedBy = []; // should not clone 'replaced by' info
$scope.certificate.removeReplaces(); // should not clone 'replacement cert' info $scope.certificate.removeReplaces(); // should not clone 'replacement cert' info
if(!$scope.certificate.keyType) {
$scope.certificate.keyType = 'RSA2048'; // default algo to select during clone if backend did not return algo
}
CertificateService.getDefaults($scope.certificate); CertificateService.getDefaults($scope.certificate);
}); });

View File

@ -289,6 +289,11 @@ angular.module('lemur')
if (certificate.dnsProviderId) { if (certificate.dnsProviderId) {
certificate.dnsProvider = {id: certificate.dnsProviderId}; certificate.dnsProvider = {id: certificate.dnsProviderId};
} }
if(!certificate.keyType) {
certificate.keyType = 'RSA2048'; // default algo to select during clone if backend did not return algo
}
}); });
}; };

View File

@ -9,7 +9,6 @@ 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 unittest.mock import patch from unittest.mock import patch
from lemur.certificates.service import create_csr from lemur.certificates.service import create_csr
@ -171,10 +170,43 @@ def test_certificate_output_schema(session, certificate, issuer_plugin):
) as wrapper: ) as wrapper:
data, errors = CertificateOutputSchema().dump(certificate) data, errors = CertificateOutputSchema().dump(certificate)
assert data["issuer"] == "LemurTrustUnittestsClass1CA2018" assert data["issuer"] == "LemurTrustUnittestsClass1CA2018"
assert data["distinguishedName"] == "L=Earth,ST=N/A,C=EE,OU=Karate Lessons,O=Daniel San & co,CN=san.example.org"
# Authority does not have 'cab_compliant', thus subject details should not be returned
assert "organization" not in data
assert wrapper.call_count == 1 assert wrapper.call_count == 1
def test_certificate_output_schema_subject_details(session, certificate, issuer_plugin):
from lemur.certificates.schemas import CertificateOutputSchema
from lemur.authorities.service import update_options
# Mark authority as non-cab-compliant
update_options(certificate.authority.id, '[{"name": "cab_compliant","value":false}]')
data, errors = CertificateOutputSchema().dump(certificate)
assert not errors
assert data["issuer"] == "LemurTrustUnittestsClass1CA2018"
assert data["distinguishedName"] == "L=Earth,ST=N/A,C=EE,OU=Karate Lessons,O=Daniel San & co,CN=san.example.org"
# Original subject details should be returned because of cab_compliant option update above
assert data["country"] == "EE"
assert data["state"] == "N/A"
assert data["location"] == "Earth"
assert data["organization"] == "Daniel San & co"
assert data["organizationalUnit"] == "Karate Lessons"
# Mark authority as cab-compliant
update_options(certificate.authority.id, '[{"name": "cab_compliant","value":true}]')
data, errors = CertificateOutputSchema().dump(certificate)
assert not errors
assert "country" not in data
assert "state" not in data
assert "location" not in data
assert "organization" not in data
assert "organizationalUnit" not in data
def test_certificate_edit_schema(session): def test_certificate_edit_schema(session):
from lemur.certificates.schemas import CertificateEditInputSchema from lemur.certificates.schemas import CertificateEditInputSchema
@ -759,12 +791,22 @@ def test_reissue_certificate(
issuer_plugin, crypto_authority, certificate, logged_in_user issuer_plugin, crypto_authority, certificate, logged_in_user
): ):
from lemur.certificates.service import reissue_certificate from lemur.certificates.service import reissue_certificate
from lemur.authorities.service import update_options
from lemur.tests.conf import LEMUR_DEFAULT_ORGANIZATION
# test-authority would return a mismatching private key, so use 'cryptography-issuer' plugin instead. # test-authority would return a mismatching private key, so use 'cryptography-issuer' plugin instead.
certificate.authority = crypto_authority certificate.authority = crypto_authority
new_cert = reissue_certificate(certificate) new_cert = reissue_certificate(certificate)
assert new_cert assert new_cert
assert (new_cert.key_type == "RSA2048") assert new_cert.key_type == "RSA2048"
assert new_cert.organization != certificate.organization
# Check for default value since authority does not have cab_compliant option set
assert new_cert.organization == LEMUR_DEFAULT_ORGANIZATION
# update cab_compliant option to false for crypto_authority to maintain subject details
update_options(crypto_authority.id, '[{"name": "cab_compliant","value":false}]')
new_cert = reissue_certificate(certificate)
assert new_cert.organization == certificate.organization
def test_create_csr(): def test_create_csr():
@ -921,6 +963,14 @@ def test_certificate_get_body(client):
"CN=LemurTrust Unittests Class 1 CA 2018" "CN=LemurTrust Unittests Class 1 CA 2018"
) )
# No authority details are provided in this test, no information about being cab_compliant is available.
# Thus original subject details should be returned.
assert response_body["country"] == "EE"
assert response_body["state"] == "N/A"
assert response_body["location"] == "Earth"
assert response_body["organization"] == "LemurTrust Enterprises Ltd"
assert response_body["organizationalUnit"] == "Unittesting Operations Center"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"token,status", "token,status",

View File

@ -17,7 +17,7 @@ bcrypt==3.1.7 # via -r requirements.txt, flask-bcrypt, paramiko
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
billiard==3.6.3.0 # via -r requirements.txt, celery billiard==3.6.3.0 # via -r requirements.txt, celery
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
boto3==1.15.12 # via -r requirements.txt boto3==1.15.16 # via -r requirements.txt
botocore==1.18.16 # via -r requirements.txt, boto3, s3transfer botocore==1.18.16 # via -r requirements.txt, boto3, s3transfer
celery[redis]==4.4.2 # via -r requirements.txt celery[redis]==4.4.2 # via -r requirements.txt
certifi==2020.6.20 # via -r requirements.txt, requests certifi==2020.6.20 # via -r requirements.txt, requests

View File

@ -10,7 +10,7 @@ aws-sam-translator==1.22.0 # via cfn-lint
aws-xray-sdk==2.5.0 # via moto aws-xray-sdk==2.5.0 # via moto
bandit==1.6.2 # via -r requirements-tests.in bandit==1.6.2 # via -r requirements-tests.in
black==20.8b1 # via -r requirements-tests.in black==20.8b1 # via -r requirements-tests.in
boto3==1.15.12 # via aws-sam-translator, moto boto3==1.15.16 # via aws-sam-translator, moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.18.16 # via aws-xray-sdk, boto3, moto, s3transfer botocore==1.18.16 # via aws-xray-sdk, boto3, moto, s3transfer
certifi==2020.6.20 # via requests certifi==2020.6.20 # via requests
@ -24,7 +24,7 @@ decorator==4.4.2 # via networkx
docker==4.2.0 # via moto docker==4.2.0 # via moto
ecdsa==0.14.1 # via moto, python-jose, sshpubkeys ecdsa==0.14.1 # via moto, python-jose, sshpubkeys
factory-boy==3.1.0 # via -r requirements-tests.in factory-boy==3.1.0 # via -r requirements-tests.in
faker==4.4.0 # via -r requirements-tests.in, factory-boy faker==4.14.0 # via -r requirements-tests.in, factory-boy
fakeredis==1.4.3 # via -r requirements-tests.in fakeredis==1.4.3 # via -r requirements-tests.in
flask==1.1.2 # via pytest-flask flask==1.1.2 # via pytest-flask
freezegun==1.0.0 # via -r requirements-tests.in freezegun==1.0.0 # via -r requirements-tests.in

View File

@ -15,7 +15,7 @@ bcrypt==3.1.7 # via flask-bcrypt, paramiko
beautifulsoup4==4.9.1 # via cloudflare beautifulsoup4==4.9.1 # via cloudflare
billiard==3.6.3.0 # via celery billiard==3.6.3.0 # via celery
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.15.12 # via -r requirements.in boto3==1.15.16 # via -r requirements.in
botocore==1.18.16 # via -r requirements.in, boto3, s3transfer botocore==1.18.16 # via -r requirements.in, boto3, s3transfer
celery[redis]==4.4.2 # via -r requirements.in celery[redis]==4.4.2 # via -r requirements.in
certifi==2020.6.20 # via -r requirements.in, requests certifi==2020.6.20 # via -r requirements.in, requests