* Initial work on #74. * Fixing tests. * Adding migration script. * Excluding migrations from coverage report.
This commit is contained in:
parent
d45e7d6b85
commit
744e204817
|
@ -1,2 +1,4 @@
|
||||||
[report]
|
[report]
|
||||||
include = lemur/*.py
|
include = lemur/*.py
|
||||||
|
omit = lemur/migrations/*
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@ class Certificate(db.Model):
|
||||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||||
backref='replaced')
|
backref='replaced')
|
||||||
|
|
||||||
|
views = relationship("View", backref="certificate")
|
||||||
endpoints = relationship("Endpoint", backref='certificate')
|
endpoints = relationship("Endpoint", backref='certificate')
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
|
@ -19,6 +19,7 @@ from lemur.destinations.models import Destination
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
from lemur.authorities.models import Authority
|
from lemur.authorities.models import Authority
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
|
from lemur.users.models import View
|
||||||
|
|
||||||
from lemur.roles.models import Role
|
from lemur.roles.models import Role
|
||||||
from lemur.roles import service as role_service
|
from lemur.roles import service as role_service
|
||||||
|
@ -129,6 +130,19 @@ def update(cert_id, owner, description, notify, destinations, notifications, rep
|
||||||
return database.update(cert)
|
return database.update(cert)
|
||||||
|
|
||||||
|
|
||||||
|
def log_private_key_view(certificate, user):
|
||||||
|
"""
|
||||||
|
Creates a record each time a certificates private key is viewed.
|
||||||
|
|
||||||
|
:param certificate:
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
view = View(user_id=user.id, certificate_id=certificate.id)
|
||||||
|
database.add(view)
|
||||||
|
database.commit()
|
||||||
|
|
||||||
|
|
||||||
def create_certificate_roles(**kwargs):
|
def create_certificate_roles(**kwargs):
|
||||||
# create an role for the owner and assign it
|
# create an role for the owner and assign it
|
||||||
owner_role = role_service.get_by_name(kwargs['owner'])
|
owner_role = role_service.get_by_name(kwargs['owner'])
|
||||||
|
|
|
@ -437,17 +437,19 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||||
if not cert:
|
if not cert:
|
||||||
return dict(message="Cannot find specified certificate"), 404
|
return dict(message="Cannot find specified certificate"), 404
|
||||||
|
|
||||||
|
if not g.current_user.is_admin:
|
||||||
owner_role = role_service.get_by_name(cert.owner)
|
owner_role = role_service.get_by_name(cert.owner)
|
||||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||||
|
|
||||||
if permission.can():
|
if not permission.can():
|
||||||
|
return dict(message='You are not authorized to view this key'), 403
|
||||||
|
|
||||||
|
service.log_private_key_view(cert, g.current_user)
|
||||||
response = make_response(jsonify(key=cert.private_key), 200)
|
response = make_response(jsonify(key=cert.private_key), 200)
|
||||||
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||||
response.headers['pragma'] = 'no-cache'
|
response.headers['pragma'] = 'no-cache'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return dict(message='You are not authorized to view this key'), 403
|
|
||||||
|
|
||||||
|
|
||||||
class Certificates(AuthenticatedResource):
|
class Certificates(AuthenticatedResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -908,45 +910,40 @@ class CertificateExport(AuthenticatedResource):
|
||||||
"""
|
"""
|
||||||
cert = service.get(certificate_id)
|
cert = service.get(certificate_id)
|
||||||
|
|
||||||
owner_role = role_service.get_by_name(cert.owner)
|
if not cert:
|
||||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
return dict(message="Cannot find specified certificate"), 404
|
||||||
|
|
||||||
options = data['plugin']['plugin_options']
|
|
||||||
plugin = data['plugin']['plugin_object']
|
plugin = data['plugin']['plugin_object']
|
||||||
|
|
||||||
if plugin.requires_key:
|
if plugin.requires_key:
|
||||||
if cert.private_key:
|
if not cert.private_key:
|
||||||
if permission.can():
|
return dict(
|
||||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(
|
||||||
|
plugin.slug))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
if not g.current_user.is_admin:
|
||||||
|
owner_role = role_service.get_by_name(cert.owner)
|
||||||
|
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||||
|
|
||||||
|
if not permission.can():
|
||||||
return dict(message='You are not authorized to export this certificate.'), 403
|
return dict(message='You are not authorized to export this certificate.'), 403
|
||||||
else:
|
|
||||||
return dict(message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(plugin.slug))
|
options = data['plugin']['plugin_options']
|
||||||
else:
|
|
||||||
|
service.log_private_key_view(cert, g.current_user)
|
||||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||||
|
|
||||||
# we take a hit in message size when b64 encoding
|
# we take a hit in message size when b64 encoding
|
||||||
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data).decode('utf-8'))
|
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data).decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
class CertificateClone(AuthenticatedResource):
|
|
||||||
def __init__(self):
|
|
||||||
self.reqparse = reqparse.RequestParser()
|
|
||||||
super(CertificateExport, self).__init__()
|
|
||||||
|
|
||||||
@validate_schema(None, certificate_output_schema)
|
|
||||||
def get(self, certificate_id):
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||||
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
||||||
api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
|
api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
|
||||||
api.add_resource(CertificateClone, '/certificates/<int:certificate_id>/clone', endpoint='cloneCertificate')
|
|
||||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
|
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
|
||||||
endpoint='notificationCertificates')
|
endpoint='notificationCertificates')
|
||||||
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
|
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
|
||||||
|
|
|
@ -94,6 +94,7 @@ def configure_app(app, config=None):
|
||||||
if config and config != 'None':
|
if config and config != 'None':
|
||||||
app.config.from_object(from_file(config))
|
app.config.from_object(from_file(config))
|
||||||
|
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
app.config.from_envvar("LEMUR_CONF")
|
app.config.from_envvar("LEMUR_CONF")
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
@ -103,6 +104,30 @@ def configure_app(app, config=None):
|
||||||
else:
|
else:
|
||||||
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
||||||
|
|
||||||
|
validate_conf(app)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_conf(app):
|
||||||
|
"""
|
||||||
|
There are a few configuration variables that are 'required' by Lemur. Here
|
||||||
|
we validate those required variables are set.
|
||||||
|
"""
|
||||||
|
required_vars = [
|
||||||
|
'LEMUR_SECURITY_TEAM_EMAIL',
|
||||||
|
'LEMUR_DEFAULT_ORGANIZATIONAL_UNIT',
|
||||||
|
'LEMUR_DEFAULT_ORGANIZATION',
|
||||||
|
'LEMUR_DEFAULT_LOCATION',
|
||||||
|
'LEMUR_DEFAULT_COUNTRY',
|
||||||
|
'LEMUR_DEFAULT_STATE',
|
||||||
|
'SQLALCHEMY_DATABASE_URI'
|
||||||
|
]
|
||||||
|
|
||||||
|
for var in required_vars:
|
||||||
|
if not app.config.get(var):
|
||||||
|
raise Exception("Required variable {var} is not set, ensure that it is set in Lemur's configuration file".format(
|
||||||
|
var=var
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def configure_extensions(app):
|
def configure_extensions(app):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Adding private key auditing.
|
||||||
|
|
||||||
|
Revision ID: 6d6151f5f307
|
||||||
|
Revises: 932525b82f1a
|
||||||
|
Create Date: 2016-11-18 16:08:12.191959
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6d6151f5f307'
|
||||||
|
down_revision = '932525b82f1a'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from sqlalchemy_utils.types import ArrowType
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('views',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('viewed_at', ArrowType(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('views')
|
||||||
|
### end Alembic commands ###
|
|
@ -423,6 +423,21 @@ def test_upload_private_key_str(user):
|
||||||
assert cert
|
assert cert
|
||||||
|
|
||||||
|
|
||||||
|
def test_private_key_audit(client, certificate):
|
||||||
|
assert len(certificate.views) == 0
|
||||||
|
client.get(api.url_for(CertificatePrivateKey, certificate_id=certificate.id), headers=VALID_ADMIN_HEADER_TOKEN)
|
||||||
|
assert len(certificate.views) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("token,status", [
|
||||||
|
(VALID_USER_HEADER_TOKEN, 200),
|
||||||
|
(VALID_ADMIN_HEADER_TOKEN, 200),
|
||||||
|
('', 401)
|
||||||
|
])
|
||||||
|
def test_certificate_get_private_key(client, token, status):
|
||||||
|
assert client.get(api.url_for(Certificates, certificate_id=1), headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("token,status", [
|
@pytest.mark.parametrize("token,status", [
|
||||||
(VALID_USER_HEADER_TOKEN, 200),
|
(VALID_USER_HEADER_TOKEN, 200),
|
||||||
(VALID_ADMIN_HEADER_TOKEN, 200),
|
(VALID_ADMIN_HEADER_TOKEN, 200),
|
||||||
|
@ -513,15 +528,6 @@ def test_certificates_patch(client, token, status):
|
||||||
assert client.patch(api.url_for(CertificatesList), data={}, headers=token).status_code == status
|
assert client.patch(api.url_for(CertificatesList), data={}, headers=token).status_code == status
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("token,status", [
|
|
||||||
(VALID_USER_HEADER_TOKEN, 403),
|
|
||||||
(VALID_ADMIN_HEADER_TOKEN, 200),
|
|
||||||
('', 401)
|
|
||||||
])
|
|
||||||
def test_certificate_credentials_get(client, token, status):
|
|
||||||
assert client.get(api.url_for(CertificatePrivateKey, certificate_id=1), headers=token).status_code == status
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("token,status", [
|
@pytest.mark.parametrize("token,status", [
|
||||||
(VALID_USER_HEADER_TOKEN, 405),
|
(VALID_USER_HEADER_TOKEN, 405),
|
||||||
(VALID_ADMIN_HEADER_TOKEN, 405),
|
(VALID_ADMIN_HEADER_TOKEN, 405),
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import sys
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Boolean
|
||||||
from sqlalchemy.event import listen
|
from sqlalchemy.event import listen
|
||||||
|
|
||||||
|
from sqlalchemy_utils.types.arrow import ArrowType
|
||||||
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.models import roles_users
|
from lemur.models import roles_users
|
||||||
|
@ -37,7 +37,7 @@ class User(db.Model):
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
password = Column(String(128))
|
password = Column(String(128))
|
||||||
active = Column(Boolean())
|
active = Column(Boolean())
|
||||||
confirmed_at = Column(DateTime())
|
confirmed_at = Column(ArrowType())
|
||||||
username = Column(String(255), nullable=False, unique=True)
|
username = Column(String(255), nullable=False, unique=True)
|
||||||
email = Column(String(128), unique=True)
|
email = Column(String(128), unique=True)
|
||||||
profile_picture = Column(String(255))
|
profile_picture = Column(String(255))
|
||||||
|
@ -63,11 +63,7 @@ class User(db.Model):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if self.password:
|
if self.password:
|
||||||
if sys.version_info[0] >= 3:
|
|
||||||
self.password = bcrypt.generate_password_hash(self.password).decode('utf-8')
|
self.password = bcrypt.generate_password_hash(self.password).decode('utf-8')
|
||||||
else:
|
|
||||||
self.password = bcrypt.generate_password_hash(self.password)
|
|
||||||
return self.password
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_admin(self):
|
def is_admin(self):
|
||||||
|
@ -85,4 +81,12 @@ class User(db.Model):
|
||||||
return "User(username={username})".format(username=self.username)
|
return "User(username={username})".format(username=self.username)
|
||||||
|
|
||||||
|
|
||||||
|
class View(db.Model):
|
||||||
|
__tablename__ = 'views'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
certificate_id = Column(Integer, ForeignKey('certificates.id'))
|
||||||
|
viewed_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False)
|
||||||
|
user_id = Column(Integer, ForeignKey('users.id'))
|
||||||
|
|
||||||
|
|
||||||
listen(User, 'before_insert', hash_password)
|
listen(User, 'before_insert', hash_password)
|
||||||
|
|
Loading…
Reference in New Issue