Initial work on #74. (#514)

* Initial work on #74.

* Fixing tests.

* Adding migration script.

* Excluding migrations from coverage report.
This commit is contained in:
kevgliss 2016-11-21 09:19:14 -08:00 committed by GitHub
parent d45e7d6b85
commit 744e204817
9 changed files with 142 additions and 57 deletions

View File

@ -1,2 +1,4 @@
[report] [report]
include = lemur/*.py include = lemur/*.py
omit = lemur/migrations/*

View File

@ -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):

View File

@ -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'])

View File

@ -437,16 +437,18 @@ 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
owner_role = role_service.get_by_name(cert.owner) if not g.current_user.is_admin:
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles]) owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
if permission.can(): if not permission.can():
response = make_response(jsonify(key=cert.private_key), 200) return dict(message='You are not authorized to view this key'), 403
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
response.headers['pragma'] = 'no-cache'
return response
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.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
response.headers['pragma'] = 'no-cache'
return response
class Certificates(AuthenticatedResource): class Certificates(AuthenticatedResource):
@ -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(
else: plugin.slug))
return dict(message='You are not authorized to export this certificate.'), 403
else: else:
return dict(message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(plugin.slug)) if not g.current_user.is_admin:
else: owner_role = role_service.get_by_name(cert.owner)
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options) 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
options = data['plugin']['plugin_options']
service.log_private_key_view(cert, g.current_user)
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',

View File

@ -94,14 +94,39 @@ 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))
try: else:
app.config.from_envvar("LEMUR_CONF") try:
except RuntimeError: app.config.from_envvar("LEMUR_CONF")
# look in default paths except RuntimeError:
if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")): # look in default paths
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py"))) if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
else: app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py")))
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py'))) else:
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):

View File

@ -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 ###

View File

@ -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),

View File

@ -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)

View File

@ -1,2 +1,2 @@
[tox] [tox]
envlist = py27,py35 envlist = py35