diff --git a/conntrackt/forms.py b/conntrackt/forms.py --- a/conntrackt/forms.py +++ b/conntrackt/forms.py @@ -3,7 +3,7 @@ from django.forms import ModelForm from django.forms.models import inlineformset_factory # Application imports. -from .models import Entity, Interface +from .models import Entity, Interface, Communication class EntityForm(ModelForm): @@ -56,3 +56,28 @@ class InterfaceForm(ModelForm): self.fields["description"].widget.attrs["placeholder"] = "Interface description" self.fields["address"].widget.attrs["placeholder"] = "IP address of interface" self.fields["netmask"].widget.attrs["placeholder"] = "IP address netmask" + + +class CommunicationForm(ModelForm): + """ + Implements a custom model form for communications with some styling changes. + """ + + class Meta: + model = Communication + + def __init__(self, *args, **kwargs): + """ + Initialises the form instance. Sets-up some bootstrap CSS classes for + widgets. + """ + + super(CommunicationForm, self).__init__(*args, **kwargs) + + # Update the widgets to be wider. + for field_name, field in self.fields.iteritems(): + field.widget.attrs["class"] = "span6" + + # Set-up some placeholders. + self.fields["port"].widget.attrs["placeholder"] = "Port used for communication" + self.fields["description"].widget.attrs["placeholder"] = "Communication description" diff --git a/conntrackt/models.py b/conntrackt/models.py --- a/conntrackt/models.py +++ b/conntrackt/models.py @@ -301,3 +301,31 @@ class Communication(models.Model): """ return "Edit" + + def source_representation(self): + """ + Produces string representation of communication that includes only the + source interface information. + + The method is useful where the destination context is well known. + + Returns: + Communication representation that includes only information about + the source interface. + """ + + return "%s - %s: %d" % (self.source, self.protocol, self.port) + + def destination_representation(self): + """ + Produces string representation of communication that includes only the + destination interface information. + + The method is useful where the source context is well known. + + Returns: + Communication representation that includes only information about + the destination interface. + """ + + return "%s - %s: %d" % (self.destination, self.protocol, self.port) diff --git a/conntrackt/templates/conntrackt/communication_confirm_delete.html b/conntrackt/templates/conntrackt/communication_confirm_delete.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/communication_confirm_delete.html @@ -0,0 +1,27 @@ +{% extends "conntrackt/base.html" %} + +{# For html_link #} +{% load conntrackt_tags %} +{# For Bootstrapped forms #} +{% load crispy_forms_tags %} + +{% block content %} +
+

Remove communication {{communication}}

+
+
+
+
+
+ {% csrf_token %} + {{ form }} + Are you sure you want to remove this communication? +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/conntrackt/templates/conntrackt/communication_create_form.html b/conntrackt/templates/conntrackt/communication_create_form.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/communication_create_form.html @@ -0,0 +1,27 @@ +{% extends "conntrackt/base.html" %} + +{# For html_link #} +{% load conntrackt_tags %} +{# For Bootstrapped forms #} +{% load crispy_forms_tags %} + +{% block content %} +
+

Add new communication

+
+ +
+
+
+
+ {% csrf_token %} + {{ form | crispy }} + {{ interface_form | crispy }} +
+
+ +
+
+
+
+{% endblock content %} diff --git a/conntrackt/templates/conntrackt/communication_update_form.html b/conntrackt/templates/conntrackt/communication_update_form.html new file mode 100644 --- /dev/null +++ b/conntrackt/templates/conntrackt/communication_update_form.html @@ -0,0 +1,25 @@ +{% extends "conntrackt/base.html" %} + +{# For html_link #} +{% load conntrackt_tags %} +{# For Bootstrapped forms #} +{% load crispy_forms_tags %} + +{% block content %} +
+

Edit communication {{interface.name}}

+
+
+
+
+
+ {% csrf_token %} + {{ form | crispy }} +
+
+ +
+
+
+
+{% endblock content %} diff --git a/conntrackt/templates/conntrackt/entity_detail.html b/conntrackt/templates/conntrackt/entity_detail.html --- a/conntrackt/templates/conntrackt/entity_detail.html +++ b/conntrackt/templates/conntrackt/entity_detail.html @@ -24,9 +24,6 @@ {% html_link "Edit" "entity_update" entity.id class="btn btn-primary" %} {% html_link "Remove" "entity_delete" entity.id class="btn btn-primary" %} {% html_link "Get Iptables" 'entity_iptables' entity.id class="btn btn-primary" %} - {% with entity_id=entity.id|slugify %} - {% html_link "Add interface" "interface_create" class="btn btn-primary" get="entity="|add:entity_id %} - {% endwith %} @@ -35,12 +32,10 @@
+

General information

- - - @@ -51,56 +46,86 @@
+

Interfaces

+ {% if interfaces %}
General information
Project{% html_link project.name 'project' project.id %}
- - - {% for interface in interfaces %} - - - - - + + + + + {% endfor %}
Interfaces
{{interface.name}} ({{interface.address}}/{{interface.netmask}}){% html_link '' 'interface_update' interface.id class="btn btn-link" %}{% html_link '' 'interface_delete' interface.id class="btn btn-link" %}

{{interface.name}} ({{interface.address}}/{{interface.netmask}})

{% html_link '' 'interface_update' interface.id class="btn btn-link" %}{% html_link '' 'interface_delete' interface.id class="btn btn-link" %}
+ {% else %} +

No interfaces are defined for this entity.

+ {% endif %} +
+ {% with entity_id=entity.id|slugify %} + {% html_link "Add interface" "interface_create" class="btn btn-primary btn-mini" get="entity="|add:entity_id %} + {% endwith %} +
+

Incoming communications

+ {% if incoming_communications %} + {% for comm in incoming_communications %} - + + + - {% for comm in incoming_communications %} - {% endfor %}
Incoming communications{% html_link comm.source_representation "entity" comm.source.entity.id class="btn btn-link" %}{% html_link '' 'communication_update' comm.id class="btn btn-link" %}{% html_link '' 'communication_delete' comm.id class="btn btn-link" %}
{{comm.source}} - {{comm.protocol}}: {{comm.port}}
+ {% else %} +

No incoming communications towards this entity.

+ {% endif %} +
+ {% with entity_id=entity.id|slugify %} + {% html_link "Add communication" "communication_create" class="btn btn-primary btn-mini" get="to_entity="|add:entity_id %} + {% endwith %} +
+

Outgoing communications

+ {% if outgoing_communications %} - - - {% for comm in outgoing_communications %} - + + + + + {% endfor %} - -
Outgoing communications
{{comm.destination}} - {{comm.protocol}}: {{comm.port}}
{% html_link comm.destination_representation "entity" comm.destination.entity.id class="btn btn-link" %}{% html_link '' 'communication_update' comm.id class="btn btn-link" %}{% html_link '' 'communication_delete' comm.id class="btn btn-link" %}
+ {% else %} +

No outgoing communications from this entity.

+ {% endif %} +
+ {% with entity_id=entity.id|slugify %} + {% html_link "Add communication" "communication_create" class="btn btn-primary btn-mini" get="from_entity="|add:entity_id %} + {% endwith %} +
-
-

Iptables rules

+

Iptables rules

+
{{ entity_iptables }}
+
+ {% html_link "Download" 'entity_iptables' entity.id class="btn btn-primary" %} +
diff --git a/conntrackt/templates/conntrackt/project_detail.html b/conntrackt/templates/conntrackt/project_detail.html --- a/conntrackt/templates/conntrackt/project_detail.html +++ b/conntrackt/templates/conntrackt/project_detail.html @@ -23,6 +23,7 @@ {% html_link "Remove" "project_delete" project.id class="btn btn-primary" %} {% with project_id=project.id|slugify %} {% html_link "Add entity" "entity_create" class="btn btn-primary" get="project="|add:project_id %} + {% html_link "Add communication" "communication_create" class="btn btn-primary" get="project="|add:project_id %} {% endwith %}
diff --git a/conntrackt/urls.py b/conntrackt/urls.py --- a/conntrackt/urls.py +++ b/conntrackt/urls.py @@ -8,6 +8,7 @@ from .views import ProjectView, ProjectC from .views import LocationCreateView, LocationUpdateView, LocationDeleteView from .views import EntityCreateView, EntityUpdateView, EntityDeleteView from .views import InterfaceCreateView, InterfaceUpdateView, InterfaceDeleteView +from .views import CommunicationCreateView, CommunicationUpdateView, CommunicationDeleteView urlpatterns = patterns( @@ -50,6 +51,13 @@ urlpatterns = patterns( # View for deleting an interface. url(r'^interface/(?P\d+)/remove/$', InterfaceDeleteView.as_view(), name="interface_delete"), + # View for creating a new communucation. + url(r'^communication/add/$', CommunicationCreateView.as_view(), name="communication_create"), + # View for updating an existing communication. + url(r'^communication/(?P\d+)/edit/$', CommunicationUpdateView.as_view(), name="communication_update"), + # View for deleting a communication. + url(r'^communication/(?P\d+)/remove/$', CommunicationDeleteView.as_view(), name="communication_delete"), + # View for rendering iptables rules for a specific entity. url(r'^entity/(?P\d+)/iptables/$', entity_iptables, name="entity_iptables"), # View for rendering zip file with iptables rules for all entities in a project. diff --git a/conntrackt/views.py b/conntrackt/views.py --- a/conntrackt/views.py +++ b/conntrackt/views.py @@ -14,8 +14,8 @@ from django.views.generic import Templat from braces.views import MultiplePermissionsRequiredMixin # Application imports. -from .forms import EntityForm, InterfaceForm -from .models import Project, Entity, Location, Interface +from .forms import EntityForm, InterfaceForm, CommunicationForm +from .models import Project, Entity, Location, Interface, Communication from .utils import generate_entity_iptables @@ -680,3 +680,196 @@ class InterfaceDeleteView(MultiplePermis self.success_url = reverse("entity", args=(self.get_object().entity.id,)) return super(InterfaceDeleteView, self).delete(*args, **kwargs) + + +class CommunicationCreateView(MultiplePermissionsRequiredMixin, CreateView): + """ + View for creating a new communication. + """ + + model = Communication + form_class = CommunicationForm + template_name_suffix = "_create_form" + + # Required permissions + permissions = { + "all": ("conntrackt.add_communication",), + } + + # Raise authorisation denied exception for unmet permissions. + raise_exception = True + + def get_form(self, form_class): + """ + Returns an instance of form that can be used by the view. + + The method will limit the source and destination interface selection to + interfaces belonging to the same project as provided entity ID (if any + was provided). + """ + + form = super(CommunicationCreateView, self).get_form(form_class) + + # Limit the interface selection based on provided source entity, + # destination entity, or project. + entity_id = self.request.GET.get("from_entity", None) + entity_id = self.request.GET.get("to_entity", None) + project_id = self.request.GET.get("project", None) + + if project_id: + form.fields["source"].queryset = Interface.objects.filter(entity__project=project_id) + form.fields["destination"].queryset = Interface.objects.filter(entity__project=project_id) + elif entity_id: + entity = Entity.objects.get(pk=1) + form.fields["source"].queryset = Interface.objects.filter(entity__project=entity.project) + form.fields["destination"].queryset = Interface.objects.filter(entity__project=entity.project) + + return form + + def get_initial(self): + """ + Returns initial values that should be pre-selected (if they were + specified through a GET parameter). + """ + + initial = super(CommunicationCreateView, self).get_initial() + + # If source or destination entity were specified in request, fetch the + # first interface from them and use it as initial source and destination. + from_entity = self.request.GET.get("from_entity", None) + to_entity = self.request.GET.get("to_entity", None) + + if from_entity: + try: + interface = Interface.objects.filter(entity=from_entity)[0] + initial["source"] = interface.id + except IndexError: + pass + + if to_entity: + try: + interface = Interface.objects.filter(entity=to_entity)[0] + initial["destination"] = interface.id + except IndexError: + pass + + return initial + + def get_success_url(self): + """ + Returns the URL to which the user should be redirected after a + communication has been created. + + The URL will be set to entity details page of an entity that was + provided as part of the from/to GET request (in that order), or as a + fallback it'll direct the user to source interface's entity details + page. + """ + + entity_id = self.request.GET.get("from_entity", None) + entity_id = self.request.GET.get("to_entity", None) + + if entity_id is None: + entity_id = self.object.source.entity.pk + + return reverse("entity", args=(entity_id,)) + + +class CommunicationUpdateView(MultiplePermissionsRequiredMixin, UpdateView): + """ + View for updating an existing communication. + """ + + model = Communication + form_class = CommunicationForm + template_name_suffix = "_update_form" + + # Required permissions. + permissions = { + "all": ("conntrackt.change_communication",), + } + + # Raise authorisation denied exception for unmet permissions. + raise_exception = True + + def get_form(self, form_class): + """ + Returns an instance of form that can be used by the view. + + The method will limit the source and destination interfaces that can be + selected for the communication. Both will be limited to interfaces + coming from entities that belong to the same project as current + communication's source interface. + """ + + form = super(CommunicationUpdateView, self).get_form(form_class) + + project = self.object.source.entity.project + + form.fields["source"].queryset = Interface.objects.filter(entity__project=project) + form.fields["destination"].queryset = Interface.objects.filter(entity__project=project) + + return form + + def get_success_url(self): + """ + Returns the URL to which the user should be redirected after a + communication has been created. + + The URL will be set to entity details page of an entity that was + provided as part of the from/to GET request (in that order), or as a + fallback it'll direct the user to source interface's entity details + page. + """ + + entity_id = self.request.GET.get("from_entity", None) + entity_id = self.request.GET.get("to_entity", None) + + if entity_id is None: + entity_id = self.object.source.entity.pk + + return reverse("entity", args=(entity_id,)) + + +class CommunicationDeleteView(MultiplePermissionsRequiredMixin, DeleteView): + """ + View for deleting an communication. + """ + + model = Communication + + # Required permissions. + permissions = { + "all": ("conntrackt.delete_communication",), + } + + # Raise authorisation denied exception for unmet permissions. + raise_exception = True + + def post(self, *args, **kwargs): + """ + Add a success message that will be displayed to the user to confirm the + communication deletion. + """ + + messages.success(self.request, "Communication %s has been removed." % self.get_object(), extra_tags="alert alert-success") + + return super(CommunicationDeleteView, self).post(*args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Deletes the object. This method is overridden in order to obtain the + entity ID for success URL. + + @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044. + """ + + entity_id = self.request.GET.get("from_entity", None) + entity_id = self.request.GET.get("to_entity", None) + + if entity_id is None: + entity_id = self.get_object().source.entity.pk + + self.success_url = reverse("entity", args=(entity_id,)) + + return super(CommunicationDeleteView, self).delete(*args, **kwargs)