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}}
+
+
+{% 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
+
+
+
+{% 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}}
+
+
+{% 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
- General information |
-
-
Project | {% html_link project.name 'project' project.id %} |
@@ -51,56 +46,86 @@
+
Interfaces
+ {% if interfaces %}
-
- Interfaces |
-
{% for interface in 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" %} |
+
{% endfor %}
+ {% 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 %}
- 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" %} |
- {% for comm in incoming_communications %}
- {{comm.source}} - {{comm.protocol}}: {{comm.port}} |
{% endfor %}
+ {% 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 %}
-
- Outgoing communications |
-
{% for comm in 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" %} |
+
{% endfor %}
-
-
+ {% 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)