Re-working the way audit logs work.

* Adding more checks.
This commit is contained in:
kevgliss 2016-11-21 11:28:11 -08:00 committed by GitHub
parent 744e204817
commit 6eca2eb147
19 changed files with 288 additions and 41 deletions

View File

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

View File

@ -10,8 +10,6 @@ addons:
matrix:
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.5"
env: TOXENV=py35

View File

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

View File

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

View File

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

View File

@ -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
lemur/logs/__init__.py Normal file
View File

23
lemur/logs/models.py Normal file
View File

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

23
lemur/logs/schemas.py Normal file
View File

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

54
lemur/logs/service.py Normal file
View File

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

74
lemur/logs/views.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
lemur/tests/test_logs.py Normal file
View File

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

View File

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