Re-working the way audit logs work.
* Adding more checks.
This commit is contained in:
parent
744e204817
commit
6eca2eb147
|
@ -3,3 +3,8 @@
|
|||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: flake8
|
||||
- id: check-merge-conflict
|
||||
- repo: git://github.com/pre-commit/mirrors-jshint
|
||||
sha: e72140112bdd29b18b0c8257956c896c4c3cebcb
|
||||
hooks:
|
||||
- id: jshint
|
||||
|
|
|
@ -10,8 +10,6 @@ addons:
|
|||
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from lemur.plugins.views import mod as plugins_bp
|
|||
from lemur.notifications.views import mod as notifications_bp
|
||||
from lemur.sources.views import mod as sources_bp
|
||||
from lemur.endpoints.views import mod as endpoints_bp
|
||||
from lemur.logs.views import mod as logs_bp
|
||||
|
||||
from lemur.__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
|
@ -49,7 +50,8 @@ LEMUR_BLUEPRINTS = (
|
|||
plugins_bp,
|
||||
notifications_bp,
|
||||
sources_bp,
|
||||
endpoints_bp
|
||||
endpoints_bp,
|
||||
logs_bp
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ class Certificate(db.Model):
|
|||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||
backref='replaced')
|
||||
|
||||
views = relationship("View", backref="certificate")
|
||||
logs = relationship("Log", backref="certificate")
|
||||
endpoints = relationship("Endpoint", backref='certificate')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
|
|
@ -10,6 +10,11 @@ import arrow
|
|||
from sqlalchemy import func, or_
|
||||
from flask import current_app
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.plugins.base import plugins
|
||||
|
@ -19,16 +24,10 @@ 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
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
|
||||
def get(cert_id):
|
||||
"""
|
||||
|
@ -130,19 +129,6 @@ 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'])
|
||||
|
|
|
@ -22,6 +22,7 @@ from lemur.certificates.schemas import certificate_input_schema, certificate_out
|
|||
certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.logs import service as log_service
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
|
@ -444,7 +445,7 @@ class CertificatePrivateKey(AuthenticatedResource):
|
|||
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)
|
||||
log_service.create(g.current_user, 'key_view', certificate=cert)
|
||||
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'
|
||||
|
@ -931,7 +932,7 @@ class CertificateExport(AuthenticatedResource):
|
|||
|
||||
options = data['plugin']['plugin_options']
|
||||
|
||||
service.log_private_key_view(cert, g.current_user)
|
||||
log_service.create(g.current_user, 'key_view', certificate=cert)
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
|
||||
# we take a hit in message size when b64 encoding
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
"""
|
||||
.. module: lemur.logs.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models related private key audit log.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey, PassiveDefault, func, Enum
|
||||
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
|
||||
class Log(db.Model):
|
||||
__tablename__ = 'log'
|
||||
id = Column(Integer, primary_key=True)
|
||||
certificate_id = Column(Integer, ForeignKey('certificates.id'))
|
||||
log_type = Column(Enum('key_view', name='log_type'), nullable=False)
|
||||
logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
|
@ -0,0 +1,23 @@
|
|||
"""
|
||||
.. module: lemur.logs.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields
|
||||
|
||||
from lemur.common.schema import LemurOutputSchema
|
||||
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
|
||||
|
||||
class LogOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
certificate = fields.Nested(CertificateNestedOutputSchema)
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
logged_at = fields.DateTime()
|
||||
log_type = fields.String()
|
||||
|
||||
|
||||
logs_output_schema = LogOutputSchema(many=True)
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
.. module: lemur.logs.service
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer logs in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from lemur import database
|
||||
from lemur.logs.models import Log
|
||||
|
||||
|
||||
def create(user, type, certificate=None):
|
||||
"""
|
||||
Creates logs a given action.
|
||||
|
||||
:param user:
|
||||
:param type:
|
||||
:param certificate:
|
||||
:return:
|
||||
"""
|
||||
view = Log(user_id=user.id, log_type=type, certificate_id=certificate.id)
|
||||
database.add(view)
|
||||
database.commit()
|
||||
|
||||
|
||||
def get_all():
|
||||
"""
|
||||
Retrieve all logs from the database.
|
||||
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Log)
|
||||
return database.find_all(query, Log, {}).all()
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that paginates and filters data when requested
|
||||
through the REST Api
|
||||
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Log)
|
||||
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Log, terms)
|
||||
|
||||
return database.sort_and_page(query, Log, args)
|
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
.. module: lemur.log.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.logs.schemas import logs_output_schema
|
||||
|
||||
from lemur.logs import service
|
||||
|
||||
|
||||
mod = Blueprint('logs', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class LogsList(AuthenticatedResource):
|
||||
""" Defines the 'logs' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(LogsList, self).__init__()
|
||||
|
||||
@validate_schema(None, logs_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /logs
|
||||
|
||||
The current log list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /logs HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
]
|
||||
"total": 2
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('owner', type=str, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
|
||||
api.add_resource(LogsList, '/logs', endpoint='logs')
|
|
@ -43,6 +43,7 @@ from lemur.destinations.models import Destination # noqa
|
|||
from lemur.domains.models import Domain # noqa
|
||||
from lemur.notifications.models import Notification # noqa
|
||||
from lemur.sources.models import Source # noqa
|
||||
from lemur.logs.models import Log # noqa
|
||||
|
||||
|
||||
manager = Manager(create_app)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.service('LogApi', function (LemurRestangular) {
|
||||
return LemurRestangular.all('domains');
|
||||
})
|
||||
.service('LogService', function () {
|
||||
var LogService = this;
|
||||
return LogService;
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($stateProvider) {
|
||||
$stateProvider.state('logs', {
|
||||
url: '/logs',
|
||||
templateUrl: '/angular/logs/view/view.tpl.html',
|
||||
controller: 'DomainsViewController'
|
||||
});
|
||||
})
|
||||
|
||||
.controller('DomainsViewController', function ($scope, $uibModal, DomainApi, DomainService, ngTableParams) {
|
||||
$scope.filter = {};
|
||||
$scope.logsTable = new ngTableParams({
|
||||
page: 1, // show first page
|
||||
count: 10, // count per page
|
||||
sorting: {
|
||||
id: 'desc' // initial sorting
|
||||
},
|
||||
filter: $scope.filter
|
||||
}, {
|
||||
total: 0, // length of data
|
||||
getData: function ($defer, params) {
|
||||
DomainApi.getList(params.url()).then(function (data) {
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="featurette-heading">Logs
|
||||
<span class="text-muted"><small>I see what you did there</small></span></h2>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="btn-group">
|
||||
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
|
||||
btn-checkbox-true="1"
|
||||
btn-checkbox-false="0">Filter</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table ng-table="domainsTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html">
|
||||
<tbody>
|
||||
<tr ng-repeat="log in $data track by $index">
|
||||
<td data-title="'Logged At'" sortable="'logged_at'" filter="{ 'logged_at': 'text' }">
|
||||
{{ log.logged_at }}
|
||||
</td>
|
||||
<td data-title="'User'" sortable="'user'" filter="{ 'user.email': 'text' }">
|
||||
{{ log.user.email }}
|
||||
</td>
|
||||
<td data-title="'Type'" sortable="'type'" filter="{ 'type': 'text' }">
|
||||
{{ log.type }}
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -55,13 +55,14 @@
|
|||
<li><a ui-sref="notifications">Notifications</a></li>
|
||||
<li></li>
|
||||
<li class="dropdown" uib-dropdown on-toggle="toggled(open)">
|
||||
<a href class="dropdown-toggle" uib-dropdown-toggle>Settings <span class="caret"></span></a>
|
||||
<a href class="dropdown-toggle" uib-dropdown-toggle>Admin <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ui-sref="destinations">Destinations</a></li>
|
||||
<li><a ui-sref="sources">Sources</a></li>
|
||||
<li><a ui-sref="roles">Roles</a></li>
|
||||
<li><a ui-sref="users">Users</a></li>
|
||||
<li><a ui-sref="domains">Domains</a></li>
|
||||
<li><a ui-sref="log">Log</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -423,12 +423,6 @@ 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),
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import pytest
|
||||
from lemur.tests.vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||
|
||||
from lemur.logs.views import * # noqa
|
||||
|
||||
|
||||
def test_private_key_audit(client, certificate):
|
||||
from lemur.certificates.views import CertificatePrivateKey, api
|
||||
assert len(certificate.logs) == 0
|
||||
client.get(api.url_for(CertificatePrivateKey, certificate_id=certificate.id), headers=VALID_ADMIN_HEADER_TOKEN)
|
||||
assert len(certificate.logs) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token,status", [
|
||||
(VALID_USER_HEADER_TOKEN, 200),
|
||||
(VALID_ADMIN_HEADER_TOKEN, 200),
|
||||
('', 401)
|
||||
])
|
||||
def test_get_logs(client, token, status):
|
||||
assert client.get(api.url_for(LogsList), headers=token).status_code == status
|
|
@ -9,7 +9,7 @@
|
|||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Boolean
|
||||
from sqlalchemy import Integer, String, Column, Boolean
|
||||
from sqlalchemy.event import listen
|
||||
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
|
@ -81,12 +81,4 @@ 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