Skip to content

Commit 619c68f

Browse files
authored
Merge pull request #1359 from EsupPortail/dev_v4
[RELEASE] 4.0.2
2 parents ecfa7c1 + dd47a9b commit 619c68f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+880
-367
lines changed

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
* [ ] You have read our [contribution guidelines](https://github.com/EsupPortail/Esup-Pod/blob/master/CONTRIBUTING.md).
44
* [ ] Your PR targets the `dev_v4` branch.
5-
* [ ] Your PR status is in `draft` if it's still a work in progress.
5+
* [ ] Your PR status is in `draft` if its still a work in progress.

.github/workflows/code_formatting.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Code Formatting
33

44
on:
55
push:
6-
branches: [master, develop, main, pod_V4]
6+
branches: [master, develop, main, dev_v4]
77
workflow_dispatch:
88

99
jobs:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ v8-compile-cache-0
9090
pod/db_migrations
9191
tmp/
9292
pod/.yarn
93+
94+
# Ignore vscode AI rules
95+
.github/instructions/codacy.instructions.md

CONFIGURATION_FR.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,11 @@ Mettre `USE_AI_ENHANCEMENT` à True pour activer cette application.<br>
801801
>> Les personnes ayant pour affiliation les valeurs<br>
802802
>> renseignées dans cette variable ont automatiquement<br>
803803
>> la valeur staff de leur compte à True.<br>
804+
* `ALLOWED_SUPERUSER_IPS`
805+
> valeur par défaut : `[]`
806+
>> Liste d’IP et/ou de plages depuis lesquelles le statut 'superuser'<br>
807+
>> est autorisé.<br>
808+
>> Laissez vide pour autoriser toutes les sources.<br>
804809
* `AUTH_CAS_USER_SEARCH`
805810
> valeur par défaut : `user`
806811
>> Variable utilisée pour trouver les informations de l’individu<br>
@@ -1245,7 +1250,7 @@ Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.<br>
12451250
> valeur par défaut : `False`
12461251
>> Mode webtv permet de basculer POD en une application webtv ensupprimant les boutons de connexions par exemple<br>
12471252
* `SOCIAL_SHARE`
1248-
> valeur par défaut : `['X', 'FACEBOOK', 'LINKEDIN', 'BLUESKY']`
1253+
> valeur par défaut : `['X', 'FACEBOOK', 'LINKEDIN', 'BLUESKY', 'MASTODON']`
12491254
>> Choix d'affichage des liens de partage des réseaux sociaux<br>
12501255
12511256
### Configuration de l’application meeting

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ tests:
7979

8080
# Ensure coherence of all code style
8181
pystyle:
82+
black . -l 90
8283
flake8
8384

8485
# Collects all static files inside all apps and put a copy inside the static directory declared in settings.py
@@ -92,6 +93,10 @@ createconfigs:
9293
python3 -Wd manage.py createconfiguration fr
9394
python3 -Wd manage.py createconfiguration en
9495

96+
# Create a superuser
97+
createsuperuser:
98+
python3 manage.py createsuperuser
99+
95100
# -- Docker
96101
# Use for docker run and docker exec commands
97102
-include .env.dev
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Esup-Pod IP Restriction middleware.
3+
4+
Ensure that only allowed IPs can access superuser privileges.
5+
"""
6+
7+
import ipaddress
8+
from django.utils.translation import gettext_lazy as _
9+
10+
11+
def ip_in_allowed_range(ip) -> bool:
12+
"""Make sure the IP is one of the authorized ones."""
13+
from django.conf import settings
14+
15+
ALLOWED_SUPERUSER_IPS = getattr(settings, "ALLOWED_SUPERUSER_IPS", [])
16+
17+
try:
18+
ip_obj = ipaddress.ip_address(ip)
19+
except ValueError:
20+
return False
21+
22+
if not ALLOWED_SUPERUSER_IPS:
23+
# Allow every clients
24+
return True
25+
26+
for allowed in ALLOWED_SUPERUSER_IPS:
27+
try:
28+
if is_allowed(ip_obj, allowed):
29+
return True
30+
except ValueError:
31+
continue
32+
return False
33+
34+
35+
def is_allowed(ip_obj, allowed):
36+
"""Check if ip object is included in allowed list."""
37+
if "/" in allowed:
38+
net = ipaddress.ip_network(allowed, strict=False)
39+
if ip_obj in net:
40+
return True
41+
else:
42+
if ip_obj == ipaddress.ip_address(allowed):
43+
return True
44+
return False
45+
46+
47+
class IPRestrictionMiddleware:
48+
def __init__(self, get_response) -> None:
49+
self.get_response = get_response
50+
51+
def __call__(self, request):
52+
ip = request.META.get("REMOTE_ADDR")
53+
user = request.user
54+
55+
if user.is_authenticated and user.is_superuser:
56+
if not ip_in_allowed_range(ip):
57+
user.is_superuser = False
58+
user.last_name = _(
59+
"%(last_name)s (Restricted - IP %(ip)s not allowed)"
60+
) % {"last_name": user.last_name, "ip": ip}
61+
62+
return self.get_response(request)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Esup-Pod IP Restriction Middleware test cases.
3+
4+
Run tests with:
5+
python manage.py test pod.authentication.tests.test_IPRestrictionMiddleware
6+
"""
7+
8+
from django.contrib.auth.models import User
9+
from django.test import TestCase, override_settings
10+
from django.test.client import RequestFactory
11+
from unittest import mock
12+
13+
14+
def check_ip_in_allowed_range(ip, allowed, expected) -> None:
15+
"""Helper function to test if a given IP is within allowed ranges."""
16+
with mock.patch("django.conf.settings") as settings:
17+
from pod.authentication.IPRestrictionMiddleware import ip_in_allowed_range
18+
19+
settings.ALLOWED_SUPERUSER_IPS = allowed
20+
assert ip_in_allowed_range(ip) is expected
21+
22+
23+
class IPRestrictionMiddlewareTestCase(TestCase):
24+
"""IP Restriction Middleware Test Case."""
25+
26+
def setUp(self) -> None:
27+
"""Tnitializes test users and request factory.."""
28+
29+
self.admin = User.objects.create(
30+
first_name="pod",
31+
last_name="Admin",
32+
username="admin",
33+
is_superuser=True,
34+
)
35+
36+
self.simple_user = User.objects.create(
37+
first_name="Pod",
38+
last_name="User",
39+
username="pod",
40+
)
41+
42+
self.factory = RequestFactory()
43+
44+
def test_ip_in_allowed_ranges(self) -> None:
45+
"""Tests IP range matching logic."""
46+
for params in [
47+
("192.168.1.10", ["192.168.1.0/24"], True),
48+
("192.168.1.10", ["10.0.0.0/8"], False),
49+
("10.0.0.1", ["10.0.0.1"], True),
50+
("invalid_ip", ["192.168.1.0/24"], False),
51+
("10.11.12.13", [], True),
52+
("10.11.12.13", [""], False),
53+
]:
54+
check_ip_in_allowed_range(*params)
55+
56+
def test_simpleuser(self) -> None:
57+
"""Ensures regular users are not affected by IP restrictions."""
58+
from pod.authentication.IPRestrictionMiddleware import IPRestrictionMiddleware
59+
60+
get_response = mock.MagicMock()
61+
middleware = IPRestrictionMiddleware(get_response)
62+
63+
request = self.factory.get("/")
64+
request.user = self.simple_user
65+
response = middleware(request)
66+
67+
# ensure get_response has been returned
68+
self.assertEqual(get_response.return_value, response)
69+
self.assertFalse(request.user.is_superuser)
70+
self.assertEqual(request.user.last_name, self.simple_user.last_name)
71+
72+
@override_settings(
73+
ALLOWED_SUPERUSER_IPS=["127.0.0.1/24"],
74+
)
75+
def test_superuser_ip_allowed(self) -> None:
76+
"""Verifies superuser access when IP is allowed."""
77+
from pod.authentication.IPRestrictionMiddleware import IPRestrictionMiddleware
78+
79+
get_response = mock.MagicMock()
80+
middleware = IPRestrictionMiddleware(get_response)
81+
82+
request = self.factory.get("/")
83+
request.user = self.admin
84+
self.assertTrue(request.user.is_superuser)
85+
response = middleware(request)
86+
87+
# ensure get_response has been returned
88+
self.assertEqual(get_response.return_value, response)
89+
self.assertTrue(request.user.is_superuser)
90+
self.assertEqual(request.user.last_name, self.admin.last_name)
91+
92+
@override_settings(
93+
ALLOWED_SUPERUSER_IPS=["10.0.0.0/8"],
94+
)
95+
def test_superuser_ip_not_allowed(self) -> None:
96+
"""Verifies superuser access is revoked when IP is not allowed."""
97+
from pod.authentication.IPRestrictionMiddleware import IPRestrictionMiddleware
98+
99+
get_response = mock.MagicMock()
100+
middleware = IPRestrictionMiddleware(get_response)
101+
102+
request = self.factory.get("/")
103+
request.user = self.admin
104+
self.assertTrue(request.user.is_superuser)
105+
106+
middleware(request)
107+
self.assertFalse(request.user.is_superuser)
108+
self.assertTrue("127.0.0.1" in request.user.last_name)

pod/authentication/tests/test_models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Esup-Pod authentication models test cases."""
1+
"""
2+
Esup-Pod authentication models test cases.
3+
4+
Test with `python manage.py test pod.authentication.tests.test_models`
5+
"""
26

37
from django.test import TestCase
48
from pod.authentication.models import Owner, AccessGroup

pod/chapter/views.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
from pod.main.utils import is_ajax
1919
import json
2020

21-
__AVAILABLE_ACTIONS__ = ["new", "save", "modify", "delete", "cancel", "import", "export"]
22-
2321

2422
@csrf_protect
2523
@login_required(redirect_field_name="referrer")
@@ -39,20 +37,25 @@ def video_chapter(request, slug):
3937

4038
list_chapter = video.chapter_set.all()
4139

42-
if request.method == "POST":
43-
if (
44-
request.POST.get("action")
45-
and request.POST.get("action") in __AVAILABLE_ACTIONS__
46-
):
47-
return eval(
48-
"video_chapter_{0}(request, video)".format(request.POST.get("action"))
49-
)
50-
else:
51-
return render(
52-
request,
53-
"video_chapter.html",
54-
{"video": video, "list_chapter": list_chapter},
55-
)
40+
if request.method == "POST" and request.POST.get("action"):
41+
action = request.POST["action"]
42+
ACTION_HANDLERS = {
43+
"new": video_chapter_new,
44+
"save": video_chapter_save,
45+
"modify": video_chapter_modify,
46+
"delete": video_chapter_delete,
47+
"cancel": video_chapter_cancel,
48+
"import": video_chapter_import,
49+
}
50+
if action in ACTION_HANDLERS:
51+
handler = ACTION_HANDLERS.get(action)
52+
if handler:
53+
return handler(request, video)
54+
return render(
55+
request,
56+
"video_chapter.html",
57+
{"video": video, "list_chapter": list_chapter},
58+
)
5659

5760

5861
def video_chapter_new(request, video):

pod/completion/static/css/completion.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ table#list-contributor thead th.contributor-name {
5454
.grid-list-track .thead-title {
5555
margin: 0;
5656
padding-right: 20px;
57-
color: var(--pod-primary);
5857
word-wrap: break-word;
5958
font-weight: 600;
6059
}

0 commit comments

Comments
 (0)