Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multi sur meme reseau ipv6 #6124

Merged
merged 11 commits into from
Nov 28, 2021
23 changes: 22 additions & 1 deletion templates/member/admin/memberip.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{% block content %}
<p>
{% blocktrans %}
Liste des membres dont la dernière IP connue est {{ ip }}
Liste des membres dont la dernière IP connue est <code>{{ ip }}</code>
PetitMote marked this conversation as resolved.
Show resolved Hide resolved
{% endblocktrans %}
</p>

Expand All @@ -37,4 +37,25 @@
{% endfor %}
</ul>
</div>

{# Checks if it's an IPV6 to show the members from the same IPV6 network #}
{% if ":" in ip %}
PetitMote marked this conversation as resolved.
Show resolved Hide resolved
<p>
{% blocktrans %}
Liste des membres dont la dernière IP connue fait partie du bloc <code>{{ network_ip }}</code>
PetitMote marked this conversation as resolved.
Show resolved Hide resolved
{% endblocktrans %}
</p>

<div class="members">
PetitMote marked this conversation as resolved.
Show resolved Hide resolved
<ul>
{% for member in network_members %}
<li>{% include "misc/member_item.part.html" with member=member info=member.last_visit|format_date:True avatar=True %}</li>
{% endfor %}
</ul>
</div>

<p>
En IPv6, les adresses sont attribuées par bloc d'IP. Un bot de spam peut donc facilement changer d'adresse IP au sein de ce bloc. Sont affichés ici tous les membres dont l'IPv6 fait partie du même bloc que l'IP demandée.
</p>
{% endif %}
{% endblock %}
95 changes: 95 additions & 0 deletions zds/member/tests/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _

from zds.member.views import member_from_ip
from zds.notification.models import TopicAnswerSubscription
from zds.member.factories import (
ProfileFactory,
Expand Down Expand Up @@ -1723,3 +1724,97 @@ def send_messages(messages):
)
self.assertEqual(result.status_code, 200)
self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8"))


class IpListingsTests(TestCase):
"""Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network."""

def setUp(self) -> None:
self.staff = StaffProfileFactory().user
self.regular_user = ProfileFactory()

self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54")
self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1"
self.user_ipv4_same_ip_1.user.save()

self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54")
self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2"
self.user_ipv4_same_ip_2.user.save()

self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55")
self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip"
self.user_ipv4_different_ip.user.save()

self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1"
self.user_ipv6_same_ip_1.user.save()

self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721")
self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2"
self.user_ipv6_same_ip_2.user.save()

self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493")
self.user_ipv6_same_network.user.username = "user_ipv6_same_network"
self.user_ipv6_same_network.user.save()

self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852")
self.user_ipv6_different_network.user.username = "user_ipv6_different_network"
self.user_ipv6_different_network.user.save()

def test_same_ipv4(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address]))
self.assertContains(response, self.user_ipv4_same_ip_1.user.username)
self.assertContains(response, self.user_ipv4_same_ip_2.user.username)
self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address)
self.assertNotContains(response, self.user_ipv4_different_ip.user.username)

def test_different_ipv4(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address]))
self.assertContains(response, self.user_ipv4_different_ip.user.username)
self.assertContains(response, self.user_ipv4_different_ip.last_ip_address)
self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)

def test_same_ipv6_and_same_ipv6_network(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address]))
self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
self.assertContains(response, self.user_ipv6_same_network.user.username)
self.assertNotContains(response, self.user_ipv6_different_network.user.username)

def test_same_ipv6_network_but_different_ip(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address]))
self.assertContains(response, self.user_ipv6_same_network.user.username)
self.assertContains(response, self.user_ipv6_same_ip_1.user.username)
self.assertContains(response, self.user_ipv6_same_ip_2.user.username)
self.assertNotContains(response, self.user_ipv6_different_network.user.username)

def test_different_ipv6_network(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address]))
self.assertContains(response, self.user_ipv6_different_network.user.username)
self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username)
self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username)
self.assertNotContains(response, self.user_ipv6_same_network.user.username)

def test_access_rights_to_ip_page_as_regular_user(self) -> None:
self.client.force_login(self.regular_user.user)
response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
self.assertEqual(response.status_code, 403)

def test_access_rights_to_ip_page_as_anonymous(self) -> None:
response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
self.assertEqual(response.status_code, 302)

def test_access_rights_to_ip_page_as_staff(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
self.assertEqual(response.status_code, 200)

def test_template_used_by_ip_page(self) -> None:
self.client.force_login(self.staff)
response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"]))
self.assertTemplateUsed(response, "member/admin/memberip.html")
15 changes: 13 additions & 2 deletions zds/member/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ipaddress
import uuid
from datetime import datetime, timedelta
from urllib.parse import unquote
Expand Down Expand Up @@ -1457,10 +1458,20 @@ def settings_promote(request, user_pk):
@login_required
@permission_required("member.change_profile", raise_exception=True)
def member_from_ip(request, ip_address):
"""List users connected from a particular IP."""
"""List users connected from a particular IP, and an IPV6 subnetwork."""

members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit")
return render(request, "member/admin/memberip.html", {"members": members, "ip": ip_address})
members_and_ip = {"members": members, "ip": ip_address}

if ":" in ip_address: # Check if it's an IPV6
network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block
# Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network
network_ip = str(network_ip)[:-1]
PetitMote marked this conversation as resolved.
Show resolved Hide resolved
network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit")
members_and_ip["network_members"] = network_members
members_and_ip["network_ip"] = network_ip

return render(request, "member/admin/memberip.html", members_and_ip)


@login_required
Expand Down