From ec22065a349528c3df4059b55b8982dd450a7c0a Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Wed, 22 Apr 2020 18:17:06 +0200 Subject: [PATCH] Add views to get IP / MAC history --- logs/forms.py | 57 ++++++ logs/models.py | 169 ++++++++++++++++++ logs/templates/logs/machine_history.html | 59 ++++++ .../logs/search_machine_history.html | 48 +++++ logs/views.py | 25 +++ 5 files changed, 358 insertions(+) create mode 100644 logs/forms.py create mode 100644 logs/models.py create mode 100644 logs/templates/logs/machine_history.html create mode 100644 logs/templates/logs/search_machine_history.html diff --git a/logs/forms.py b/logs/forms.py new file mode 100644 index 00000000..9e2b98a6 --- /dev/null +++ b/logs/forms.py @@ -0,0 +1,57 @@ +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2020 Jean-Romain Garnier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""The forms used by the search app""" + +from django import forms +from django.forms import Form +from django.utils.translation import ugettext_lazy as _ +from re2o.base import get_input_formats_help_text + +CHOICES_TYPE = ( + ("ip", _("IPv4")), + ("mac", _("MAC")), +) + + +class MachineHistoryForm(Form): + """The form for a simple search""" + + q = forms.CharField( + label=_("Search"), + max_length=100, + ) + t = forms.CharField( + label=_("Search type"), + widget=forms.Select, + choices=CHOICES_TYPE, + initial=0, + ) + s = forms.DateField(required=False, label=_("Start date")) + e = forms.DateField(required=False, label=_("End date")) + + def __init__(self, *args, **kwargs): + super(MachineHistoryForm, self).__init__(*args, **kwargs) + self.fields["s"].help_text = get_input_formats_help_text( + self.fields["s"].input_formats + ) + self.fields["e"].help_text = get_input_formats_help_text( + self.fields["e"].input_formats + ) diff --git a/logs/models.py b/logs/models.py new file mode 100644 index 00000000..906fc020 --- /dev/null +++ b/logs/models.py @@ -0,0 +1,169 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2020 Jean-Romain Garnier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""machines.models +The models definitions for the Machines app +""" +from reversion.models import Version +from machines.models import IpList +from machines.models import Interface +from machines.models import Machine +from users.models import User + + +class HistoryEvent: + def __init__(self, user: User, machine: Version, interface: Version, start=None, end=None): + self.user = user + self.machine = machine + self.interface = interface + self.ipv4 = IpList.objects.get(id=interface.field_dict["ipv4_id"]).ipv4 + self.mac = self.interface.field_dict["mac_address"] + self.start_date = start + self.end_date = end + self.comment = interface.revision.get_comment() or None + + def is_similar(self, elt2): + return ( + elt2 is not None + and self.user.id == elt2.user.id + and self.ipv4 == elt2.ipv4 + and self.machine.field_dict["id"] == elt2.machine.field_dict["id"] + and self.interface.field_dict["id"] == elt2.interface.field_dict["id"] + ) + + def __repr__(self): + return "{} ({} - ): from {} to {} ({})".format( + self.machine, + self.mac, + self.ipv4, + self.start_date, + self.end_date, + self.comment or "No comment" + ) + + +class MachineHistory: + def __init__(self): + self.events = [] + self.__last_evt = None + + def get(self, search, params): + self.start = params.get("s", None) + self.end = params.get("e", None) + search_type = params.get("t", 0) + + self.events = [] + if search_type == "ip": + return self.__get_by_ip(search) + elif search_type == "mac": + return self.__get_by_mac(search) + + return None + + def __add_revision(self, user: User, machine: Version, interface: Version): + evt = HistoryEvent(user, machine, interface) + evt.start_date = interface.revision.date_created + + # Try not to recreate events if unnecessary + if evt.is_similar(self.__last_evt): + return + + # Mark the end of validity of the last element + if self.__last_evt and not self.__last_evt.end_date: + self.__last_evt.end_date = evt.start_date + + # If the event ends before the given date, remove it + if self.start and evt.start_date < self.start: + self.__last_evt = None + self.events.pop() + + # Make sure the new event starts before the given end date + if self.end and evt.start_date > self.end: + return + + # Save the new element + self.events.append(evt) + self.__last_evt = evt + + def __get_interfaces_for_ip(self, ip: str): + """ + Returns an iterable object with the Version objects + of Interfaces with the given IP + """ + # TODO: Deleted IpList + ip_id = IpList.objects.get(ipv4=ip).id + return filter( + lambda x: x.field_dict["ipv4_id"] == ip_id, + Version.objects.get_for_model(Interface).order_by("revision__date_created") + ) + + def __get_interfaces_for_mac(self, mac: str): + """ + Returns an iterable object with the Version objects + of Interfaces with the given MAC + """ + # TODO: What if IpList was deleted? + return filter( + lambda x: str(x.field_dict["mac_address"]) == mac, + Version.objects.get_for_model(Interface).order_by("revision__date_created") + ) + + def __get_machines_for_interface(self, interface: Version): + """ + Returns an iterable object with the Verison objects + of Machines to which the given interface was attributed + """ + machine_id = interface.field_dict["machine_id"] + return filter( + lambda x: x.field_dict["id"] == machine_id, + Version.objects.get_for_model(Machine).order_by("revision__date_created") + ) + + def __get_user_for_machine(self, machine: Version): + """ + Returns the user to which the given machine belongs + """ + # TODO: What if user was deleted? + user_id = machine.field_dict["user_id"] + return User.objects.get(id=user_id) + + def __get_by_ip(self, ip: str): + interfaces = self.__get_interfaces_for_ip(ip) + + for interface in interfaces: + machines = self.__get_machines_for_interface(interface) + + for machine in machines: + user = self.__get_user_for_machine(machine) + self.__add_revision(user, machine, interface) + + return self.events + + def __get_by_mac(self, mac: str): + interfaces = self.__get_interfaces_for_mac(mac) + + for interface in interfaces: + machines = self.__get_machines_for_interface(interface) + + for machine in machines: + user = self.__get_user_for_machine(machine) + self.__add_revision(user, machine, interface) + + return self.events diff --git a/logs/templates/logs/machine_history.html b/logs/templates/logs/machine_history.html new file mode 100644 index 00000000..01d6c7e7 --- /dev/null +++ b/logs/templates/logs/machine_history.html @@ -0,0 +1,59 @@ +{% extends 'log/sidebar.html' %} +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2020 Jean-Romain Garnier + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "Search results" %}{% endblock %} + +{% block content %} + {% if events %} + + + + + + + + + + + + {% for event in events %} + + + + + + + + + {% endfor %} +
UserIPv4MACStart dateEnd dateComment
{{ event.user.pseudo }}{{ event.ipv4 }}{{ event.mac }}{{ event.start_date }}{{ event.end_date }}{{ event.comment }}
+ {% else %} +

{% trans "No result" %}

+ {% endif %} +
+
+
+{% endblock %} diff --git a/logs/templates/logs/search_machine_history.html b/logs/templates/logs/search_machine_history.html new file mode 100644 index 00000000..3baf563d --- /dev/null +++ b/logs/templates/logs/search_machine_history.html @@ -0,0 +1,48 @@ +{% extends 'logs/sidebar.html' %} +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2020 Jean-Romain Garnier + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "Search" %}{% endblock %} + +{% block content %} + +
+ {% bootstrap_field history_form.q %} + {% bootstrap_form_errors history_form.t %} + {% if history_form.s %} + {% bootstrap_field history_form.s %} + {% endif %} + {% if history_form.e %} + {% bootstrap_field history_form.e %} + {% endif %} + {% trans "Search" as tr_search %} + {% bootstrap_button tr_search button_type="submit" icon="search" %} +
+
+
+
+
+
+{% endblock %} diff --git a/logs/views.py b/logs/views.py index 78971d18..08ac0c9f 100644 --- a/logs/views.py +++ b/logs/views.py @@ -101,6 +101,9 @@ from re2o.utils import ( from re2o.base import re2o_paginator, SortTable from re2o.acl import can_view_all, can_view_app, can_edit_history +from .models import MachineHistory +from .forms import MachineHistoryForm + @login_required @can_view_app("logs") @@ -478,6 +481,28 @@ def stats_actions(request): return render(request, "logs/stats_users.html", {"stats_list": stats}) +@login_required +@can_view_app("users") +def search_machine_history(request): + """Vue qui permet de rechercher l'historique des machines ayant utilisé + une IP ou une adresse MAC""" + history_form = MachineHistoryForm(request.GET or None) + if history_form.is_valid(): + history = MachineHistory() + return render( + request, + "logs/machine_history.html", + { + "events": + history.get( + history_form.cleaned_data.get("q", ""), + history_form.cleaned_data + ) + }, + ) + return render(request, "logs/search_machine_history.html", {"history_form": history_form}) + + def history(request, application, object_name, object_id): """Render history for a model.