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: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: flake8 - 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: matrix:
include: include:
- python: "2.7"
env: TOXENV=py27
- python: "3.5" - python: "3.5"
env: TOXENV=py35 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.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp from lemur.sources.views import mod as sources_bp
from lemur.endpoints.views import mod as endpoints_bp from lemur.endpoints.views import mod as endpoints_bp
from lemur.logs.views import mod as logs_bp
from lemur.__about__ import ( from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__, __author__, __copyright__, __email__, __license__, __summary__, __title__,
@ -49,7 +50,8 @@ LEMUR_BLUEPRINTS = (
plugins_bp, plugins_bp,
notifications_bp, notifications_bp,
sources_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 secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced') backref='replaced')
views = relationship("View", 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):

View File

@ -10,6 +10,11 @@ import arrow
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from flask import current_app 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 import database
from lemur.extensions import metrics from lemur.extensions import metrics
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
@ -19,16 +24,10 @@ from lemur.destinations.models import Destination
from lemur.notifications.models import Notification from lemur.notifications.models import Notification
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.users.models import View
from lemur.roles.models import Role from lemur.roles.models import Role
from lemur.roles import service as role_service 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): def get(cert_id):
""" """
@ -130,19 +129,6 @@ def update(cert_id, owner, description, notify, destinations, notifications, rep
return database.update(cert) 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): def create_certificate_roles(**kwargs):
# create an role for the owner and assign it # create an role for the owner and assign it
owner_role = role_service.get_by_name(kwargs['owner']) 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 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.roles import service as role_service
from lemur.logs import service as log_service
mod = Blueprint('certificates', __name__) mod = Blueprint('certificates', __name__)
@ -444,7 +445,7 @@ class CertificatePrivateKey(AuthenticatedResource):
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
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 = make_response(jsonify(key=cert.private_key), 200)
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store' response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
response.headers['pragma'] = 'no-cache' response.headers['pragma'] = 'no-cache'
@ -931,7 +932,7 @@ class CertificateExport(AuthenticatedResource):
options = data['plugin']['plugin_options'] 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) extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
# we take a hit in message size when b64 encoding # 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.domains.models import Domain # noqa
from lemur.notifications.models import Notification # noqa from lemur.notifications.models import Notification # noqa
from lemur.sources.models import Source # noqa from lemur.sources.models import Source # noqa
from lemur.logs.models import Log # noqa
manager = Manager(create_app) 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><a ui-sref="notifications">Notifications</a></li>
<li></li> <li></li>
<li class="dropdown" uib-dropdown on-toggle="toggled(open)"> <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"> <ul class="dropdown-menu">
<li><a ui-sref="destinations">Destinations</a></li> <li><a ui-sref="destinations">Destinations</a></li>
<li><a ui-sref="sources">Sources</a></li> <li><a ui-sref="sources">Sources</a></li>
<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>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -423,12 +423,6 @@ def test_upload_private_key_str(user):
assert cert 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", [ @pytest.mark.parametrize("token,status", [
(VALID_USER_HEADER_TOKEN, 200), (VALID_USER_HEADER_TOKEN, 200),
(VALID_ADMIN_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> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from sqlalchemy.orm import relationship 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.event import listen
from sqlalchemy_utils.types.arrow import ArrowType from sqlalchemy_utils.types.arrow import ArrowType
@ -81,12 +81,4 @@ class User(db.Model):
return "User(username={username})".format(username=self.username) 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) listen(User, 'before_insert', hash_password)