From 6eca2eb147458e0068c33a7ace1f26a05c83bda1 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 21 Nov 2016 11:28:11 -0800 Subject: [PATCH] Re-working the way audit logs work. * Adding more checks. --- .pre-commit-config.yaml | 5 ++ .travis.yml | 2 - lemur/__init__.py | 4 +- lemur/certificates/models.py | 2 +- lemur/certificates/service.py | 24 ++---- lemur/certificates/views.py | 5 +- lemur/logs/__init__.py | 0 lemur/logs/models.py | 23 ++++++ lemur/logs/schemas.py | 23 ++++++ lemur/logs/service.py | 54 ++++++++++++++ lemur/logs/views.py | 74 +++++++++++++++++++ lemur/manage.py | 1 + lemur/static/app/angular/logs/services.js | 10 +++ lemur/static/app/angular/logs/view/view.js | 31 ++++++++ .../app/angular/logs/view/view.tpl.html | 32 ++++++++ lemur/static/app/index.html | 3 +- lemur/tests/test_certificates.py | 6 -- lemur/tests/test_logs.py | 20 +++++ lemur/users/models.py | 10 +-- 19 files changed, 288 insertions(+), 41 deletions(-) create mode 100644 lemur/logs/__init__.py create mode 100644 lemur/logs/models.py create mode 100644 lemur/logs/schemas.py create mode 100644 lemur/logs/service.py create mode 100644 lemur/logs/views.py create mode 100644 lemur/static/app/angular/logs/services.js create mode 100644 lemur/static/app/angular/logs/view/view.js create mode 100644 lemur/static/app/angular/logs/view/view.tpl.html create mode 100644 lemur/tests/test_logs.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4c598af..0855b3de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.travis.yml b/.travis.yml index edbd0df9..27d5a946 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,6 @@ addons: matrix: include: - - python: "2.7" - env: TOXENV=py27 - python: "3.5" env: TOXENV=py35 diff --git a/lemur/__init__.py b/lemur/__init__.py index cb05a768..273bd349 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -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 ) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index c5d740f5..01966537 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -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): diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index cae7d9ed..450b82e4 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -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']) diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index cdf78178..89038718 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -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 diff --git a/lemur/logs/__init__.py b/lemur/logs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/logs/models.py b/lemur/logs/models.py new file mode 100644 index 00000000..7595a351 --- /dev/null +++ b/lemur/logs/models.py @@ -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 +""" +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) diff --git a/lemur/logs/schemas.py b/lemur/logs/schemas.py new file mode 100644 index 00000000..db16755a --- /dev/null +++ b/lemur/logs/schemas.py @@ -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 +""" +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) diff --git a/lemur/logs/service.py b/lemur/logs/service.py new file mode 100644 index 00000000..18cd5a91 --- /dev/null +++ b/lemur/logs/service.py @@ -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 +""" +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) diff --git a/lemur/logs/views.py b/lemur/logs/views.py new file mode 100644 index 00000000..7c32c747 --- /dev/null +++ b/lemur/logs/views.py @@ -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 +""" +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') diff --git a/lemur/manage.py b/lemur/manage.py index 4b95a183..e49cadb0 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -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) diff --git a/lemur/static/app/angular/logs/services.js b/lemur/static/app/angular/logs/services.js new file mode 100644 index 00000000..4ab0821b --- /dev/null +++ b/lemur/static/app/angular/logs/services.js @@ -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; + }); diff --git a/lemur/static/app/angular/logs/view/view.js b/lemur/static/app/angular/logs/view/view.js new file mode 100644 index 00000000..9ec4c9e6 --- /dev/null +++ b/lemur/static/app/angular/logs/view/view.js @@ -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); + }); + } + }); + }); diff --git a/lemur/static/app/angular/logs/view/view.tpl.html b/lemur/static/app/angular/logs/view/view.tpl.html new file mode 100644 index 00000000..f3430f04 --- /dev/null +++ b/lemur/static/app/angular/logs/view/view.tpl.html @@ -0,0 +1,32 @@ +
+
+

Logs + I see what you did there

+
+
+
+ +
+
+
+
+ + + + + + + +
+ {{ log.logged_at }} + + {{ log.user.email }} + + {{ log.type }} +
+
+
+
+
diff --git a/lemur/static/app/index.html b/lemur/static/app/index.html index 3dcfeb8c..9b7de523 100644 --- a/lemur/static/app/index.html +++ b/lemur/static/app/index.html @@ -55,13 +55,14 @@
  • Notifications
  • diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 73c28ce9..67e99a65 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -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), diff --git a/lemur/tests/test_logs.py b/lemur/tests/test_logs.py new file mode 100644 index 00000000..34b49a2e --- /dev/null +++ b/lemur/tests/test_logs.py @@ -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 diff --git a/lemur/users/models.py b/lemur/users/models.py index 2a31caa5..7c8f5f18 100644 --- a/lemur/users/models.py +++ b/lemur/users/models.py @@ -9,7 +9,7 @@ .. moduleauthor:: Kevin Glisson """ 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)