* 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]
|
||||
include = lemur/*.py
|
||||
omit = lemur/migrations/*
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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'])
|
||||
|
@ -437,16 +437,18 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
if not cert:
|
||||
return dict(message="Cannot find specified certificate"), 404
|
||||
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||
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():
|
||||
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
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to view this key'), 403
|
||||
|
||||
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):
|
||||
@ -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)
|
||||
else:
|
||||
return dict(message='You are not authorized to export this certificate.'), 403
|
||||
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:
|
||||
return dict(message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(plugin.slug))
|
||||
else:
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
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
|
||||
|
||||
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',
|
||||
|
@ -94,14 +94,39 @@ def configure_app(app, config=None):
|
||||
if config and config != 'None':
|
||||
app.config.from_object(from_file(config))
|
||||
|
||||
try:
|
||||
app.config.from_envvar("LEMUR_CONF")
|
||||
except RuntimeError:
|
||||
# look in default paths
|
||||
if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
|
||||
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py")))
|
||||
else:
|
||||
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
||||
else:
|
||||
try:
|
||||
app.config.from_envvar("LEMUR_CONF")
|
||||
except RuntimeError:
|
||||
# look in default paths
|
||||
if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
|
||||
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.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):
|
||||
|
36
lemur/migrations/versions/6d6151f5f307_.py
Normal file
36
lemur/migrations/versions/6d6151f5f307_.py
Normal 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 ###
|
@ -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),
|
||||
|
@ -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
|
||||
self.password = bcrypt.generate_password_hash(self.password).decode('utf-8')
|
||||
|
||||
@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)
|
||||
|
Loading…
Reference in New Issue
Block a user