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]
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
backref='replaced')
views = relationship("View", backref="certificate")
endpoints = relationship("Endpoint", backref='certificate')
def __init__(self, **kwargs):

View File

@ -19,6 +19,7 @@ from lemur.destinations.models import Destination
from lemur.notifications.models import Notification
from lemur.authorities.models import Authority
from lemur.domains.models import Domain
from lemur.users.models import View
from lemur.roles.models import Role
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)
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):
# create an role for the owner and assign it
owner_role = role_service.get_by_name(kwargs['owner'])

View File

@ -437,17 +437,19 @@ class CertificatePrivateKey(AuthenticatedResource):
if not cert:
return dict(message="Cannot find specified certificate"), 404
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 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.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
class Certificates(AuthenticatedResource):
def __init__(self):
@ -908,45 +910,40 @@ class CertificateExport(AuthenticatedResource):
"""
cert = service.get(certificate_id)
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
if not cert:
return dict(message="Cannot find specified certificate"), 404
options = data['plugin']['plugin_options']
plugin = data['plugin']['plugin_object']
if plugin.requires_key:
if cert.private_key:
if permission.can():
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
if not cert.private_key:
return dict(
message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(
plugin.slug))
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
else:
return dict(message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(plugin.slug))
else:
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
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(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
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(CertificateClone, '/certificates/<int:certificate_id>/clone', endpoint='cloneCertificate')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
endpoint='notificationCertificates')
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',

View File

@ -94,6 +94,7 @@ def configure_app(app, config=None):
if config and config != 'None':
app.config.from_object(from_file(config))
else:
try:
app.config.from_envvar("LEMUR_CONF")
except RuntimeError:
@ -103,6 +104,30 @@ def configure_app(app, config=None):
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):
"""

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
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", [
(VALID_USER_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
@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", [
(VALID_USER_HEADER_TOKEN, 405),
(VALID_ADMIN_HEADER_TOKEN, 405),

View File

@ -8,11 +8,11 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import sys
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_utils.types.arrow import ArrowType
from lemur.database import db
from lemur.models import roles_users
@ -37,7 +37,7 @@ class User(db.Model):
id = Column(Integer, primary_key=True)
password = Column(String(128))
active = Column(Boolean())
confirmed_at = Column(DateTime())
confirmed_at = Column(ArrowType())
username = Column(String(255), nullable=False, unique=True)
email = Column(String(128), unique=True)
profile_picture = Column(String(255))
@ -63,11 +63,7 @@ class User(db.Model):
:return:
"""
if self.password:
if sys.version_info[0] >= 3:
self.password = bcrypt.generate_password_hash(self.password).decode('utf-8')
else:
self.password = bcrypt.generate_password_hash(self.password)
return self.password
@property
def is_admin(self):
@ -85,4 +81,12 @@ class User(db.Model):
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)

View File

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