Log fixes (#534)

* tying up some loose ends with event logging

* Ensuring creators can access
This commit is contained in:
kevgliss 2016-11-28 14:13:16 -08:00 committed by GitHub
parent e2143d3ee8
commit 727bc87ede
11 changed files with 70 additions and 65 deletions

View File

@ -15,9 +15,6 @@ from flask_principal import Permission, RoleNeed
operator_permission = Permission(RoleNeed('operator')) operator_permission = Permission(RoleNeed('operator'))
admin_permission = Permission(RoleNeed('admin')) admin_permission = Permission(RoleNeed('admin'))
CertificateCreator = namedtuple('certificate', ['method', 'value'])
CertificateCreatorNeed = partial(CertificateCreator, 'key')
CertificateOwner = namedtuple('certificate', ['method', 'value']) CertificateOwner = namedtuple('certificate', ['method', 'value'])
CertificateOwnerNeed = partial(CertificateOwner, 'role') CertificateOwnerNeed = partial(CertificateOwner, 'role')
@ -28,8 +25,8 @@ class SensitiveDomainPermission(Permission):
class CertificatePermission(Permission): class CertificatePermission(Permission):
def __init__(self, certificate_id, owner, roles): def __init__(self, owner, roles):
needs = [RoleNeed('admin'), CertificateCreatorNeed(certificate_id), RoleNeed(owner)] needs = [RoleNeed('admin'), RoleNeed(owner), RoleNeed('creator')]
for r in roles: for r in roles:
needs.append(CertificateOwnerNeed(str(r))) needs.append(CertificateOwnerNeed(str(r)))

View File

@ -27,8 +27,7 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from lemur.users import service as user_service from lemur.users import service as user_service
from lemur.auth.permissions import CertificateCreatorNeed, \ from lemur.auth.permissions import AuthorityCreatorNeed, RoleMemberNeed
AuthorityCreatorNeed, RoleMemberNeed
def get_rsa_public_key(n, e): def get_rsa_public_key(n, e):
@ -155,11 +154,6 @@ def on_identity_loaded(sender, identity):
for authority in user.authorities: for authority in user.authorities:
identity.provides.add(AuthorityCreatorNeed(authority.id)) identity.provides.add(AuthorityCreatorNeed(authority.id))
# apply ownership of certificates
if hasattr(user, 'certificates'):
for certificate in user.certificates:
identity.provides.add(CertificateCreatorNeed(certificate.id))
g.user = user g.user = user

View File

@ -68,19 +68,19 @@ class Certificate(db.Model):
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE")) authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE")) root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') notifications = relationship('Notification', secondary=certificate_notification_associations, backref='certificate')
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') destinations = relationship('Destination', secondary=certificate_destination_associations, backref='certificate')
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate') sources = relationship('Source', secondary=certificate_source_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate") domains = relationship('Domain', secondary=certificate_associations, backref='certificate')
roles = relationship("Role", secondary=roles_certificates, backref="certificate") roles = relationship('Role', secondary=roles_certificates, backref='certificate')
replaces = relationship("Certificate", replaces = relationship('Certificate',
secondary=certificate_replacement_associations, secondary=certificate_replacement_associations,
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced') backref='replaced')
logs = relationship("Log", backref="certificate") logs = relationship('Log', backref='certificate')
endpoints = relationship("Endpoint", backref='certificate') endpoints = relationship('Endpoint', backref='certificate')
def __init__(self, **kwargs): def __init__(self, **kwargs):
cert = lemur.common.utils.parse_certificate(kwargs['body']) cert = lemur.common.utils.parse_certificate(kwargs['body'])

View File

@ -438,9 +438,10 @@ 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: # allow creators
if g.current_user != cert.user:
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(owner_role, [x.name for x in cert.roles])
if not permission.can(): 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
@ -621,10 +622,17 @@ class Certificates(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
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
return dict(message='You are not authorized to update this certificate'), 403
if permission.can():
for destination in data['destinations']: for destination in data['destinations']:
if destination.plugin.requires_key: if destination.plugin.requires_key:
if not cert.private_key: if not cert.private_key:
@ -641,8 +649,6 @@ class Certificates(AuthenticatedResource):
data['roles'] data['roles']
) )
return dict(message='You are not authorized to update this certificate'), 403
class NotificationCertificatesList(AuthenticatedResource): class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """ """ Defines the 'certificates' endpoint """
@ -923,9 +929,10 @@ class CertificateExport(AuthenticatedResource):
plugin.slug)) plugin.slug))
else: else:
if not g.current_user.is_admin: # allow creators
if g.current_user != cert.user:
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(owner_role, [x.name for x in cert.roles])
if not permission.can(): 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

View File

@ -15,7 +15,7 @@ from lemur.database import db
class Log(db.Model): class Log(db.Model):
__tablename__ = 'log' __tablename__ = 'logs'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
certificate_id = Column(Integer, ForeignKey('certificates.id')) certificate_id = Column(Integer, ForeignKey('certificates.id'))
log_type = Column(Enum('key_view', name='log_type'), nullable=False) log_type = Column(Enum('key_view', name='log_type'), nullable=False)

View File

@ -1,28 +1,27 @@
"""Adding private key auditing. """Adding logging database tables.
Revision ID: 6d6151f5f307 Revision ID: e3691fc396e9
Revises: 932525b82f1a Revises: 932525b82f1a
Create Date: 2016-11-18 16:08:12.191959 Create Date: 2016-11-28 13:15:46.995219
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '6d6151f5f307' revision = 'e3691fc396e9'
down_revision = '932525b82f1a' down_revision = '932525b82f1a'
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy_utils
from sqlalchemy_utils.types import ArrowType
def upgrade(): def upgrade():
### commands auto generated by Alembic - please adjust! ### ### commands auto generated by Alembic - please adjust! ###
op.create_table('views', op.create_table('logs',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('certificate_id', sa.Integer(), nullable=True), sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.Column('viewed_at', ArrowType(), server_default=sa.text('now()'), nullable=False), sa.Column('log_type', sa.Enum('key_view', name='log_type'), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('logged_at', sqlalchemy_utils.types.arrow.ArrowType(), server_default=sa.text('now()'), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ), sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
@ -32,5 +31,5 @@ def upgrade():
def downgrade(): def downgrade():
### commands auto generated by Alembic - please adjust! ### ### commands auto generated by Alembic - please adjust! ###
op.drop_table('views') op.drop_table('logs')
### end Alembic commands ### ### end Alembic commands ###

View File

@ -2,7 +2,7 @@
angular.module('lemur') angular.module('lemur')
.service('LogApi', function (LemurRestangular) { .service('LogApi', function (LemurRestangular) {
return LemurRestangular.all('domains'); return LemurRestangular.all('logs');
}) })
.service('LogService', function () { .service('LogService', function () {
var LogService = this; var LogService = this;

View File

@ -6,11 +6,11 @@ angular.module('lemur')
$stateProvider.state('logs', { $stateProvider.state('logs', {
url: '/logs', url: '/logs',
templateUrl: '/angular/logs/view/view.tpl.html', templateUrl: '/angular/logs/view/view.tpl.html',
controller: 'DomainsViewController' controller: 'LogsViewController'
}); });
}) })
.controller('DomainsViewController', function ($scope, $uibModal, DomainApi, DomainService, ngTableParams) { .controller('LogsViewController', function ($scope, $uibModal, LogApi, LogService, ngTableParams, MomentService) {
$scope.filter = {}; $scope.filter = {};
$scope.logsTable = new ngTableParams({ $scope.logsTable = new ngTableParams({
page: 1, // show first page page: 1, // show first page
@ -22,10 +22,12 @@ angular.module('lemur')
}, { }, {
total: 0, // length of data total: 0, // length of data
getData: function ($defer, params) { getData: function ($defer, params) {
DomainApi.getList(params.url()).then(function (data) { LogApi.getList(params.url()).then(function (data) {
params.total(data.total); params.total(data.total);
$defer.resolve(data); $defer.resolve(data);
}); });
} }
}); });
$scope.momentService = MomentService;
}); });

View File

@ -12,17 +12,22 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table ng-table="domainsTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html"> <table ng-table="logsTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html">
<tbody> <tbody>
<tr ng-repeat="log in $data track by $index"> <tr ng-repeat="log in $data track by $index">
<td data-title="'Logged At'" sortable="'logged_at'" filter="{ 'logged_at': 'text' }"> <td data-title="'Logged'" sortable="'logged_at'" filter="{ 'logged_at': 'text' }">
{{ log.logged_at }} <span class="pull-right" uib-tooltip="{{ log.loggedAt }}">
{{ momentService.createMoment(log.loggedAt) }}
</span>
</td>
<td data-title="'Certificate'" sortable="'certificate'" filter="{ 'certificate.name': 'text' }">
{{ log.certificate.name }}
</td> </td>
<td data-title="'User'" sortable="'user'" filter="{ 'user.email': 'text' }"> <td data-title="'User'" sortable="'user'" filter="{ 'user.email': 'text' }">
{{ log.user.email }} {{ log.user.email }}
</td> </td>
<td data-title="'Type'" sortable="'type'" filter="{ 'type': 'text' }"> <td data-title="'Type'" sortable="'type'" filter="{ 'type': 'text' }">
{{ log.type }} {{ log.logType }}
</td> </td>
</tbody> </tbody>
</table> </table>

View File

@ -62,7 +62,7 @@
<li><a ui-sref="roles">Roles</a></li> <li><a ui-sref="roles">Roles</a></li>
<li><a ui-sref="users">Users</a></li> <li><a ui-sref="users">Users</a></li>
<li><a ui-sref="domains">Domains</a></li> <li><a ui-sref="domains">Domains</a></li>
<li><a ui-sref="log">Log</a></li> <li><a ui-sref="logs">Logs</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -42,8 +42,9 @@ class User(db.Model):
email = Column(String(128), unique=True) email = Column(String(128), unique=True)
profile_picture = Column(String(255)) profile_picture = Column(String(255))
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic') roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
certificates = relationship("Certificate", backref=db.backref('user'), lazy='dynamic') certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
authorities = relationship("Authority", backref=db.backref('user'), lazy='dynamic') authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
def check_password(self, password): def check_password(self, password):
""" """