1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-20 08:01:26 +02:00

Compare commits

..

4 Commits

Author SHA1 Message Date
c411197af3 multiline support for RSA key in env 2025-06-27 22:13:43 +02:00
cdc6f0a3f8 Fix jwks.json 2025-06-27 12:13:54 +02:00
df0d886db9 linters 2025-06-17 11:46:33 +02:00
092cc37320 OIDC 0 Quark 1 2025-06-17 00:38:11 +02:00
16 changed files with 58 additions and 322 deletions

View File

@ -21,3 +21,6 @@ EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration # Wiki configuration
WIKI_USER=NoteKfet2020 WIKI_USER=NoteKfet2020
WIKI_PASSWORD= WIKI_PASSWORD=
# OIDC
OIDC_RSA_PRIVATE_KEY=CHANGE_ME

View File

@ -61,8 +61,8 @@ Bien que cela permette de créer une instance sur toutes les distributions,
6. (Optionnel) **Création d'une clé privée OpenID Connect** 6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et copier la clé dans .env dans le champ
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`). `OIDC_RSA_PRIVATE_KEY`.
7. Enjoy : 7. Enjoy :
@ -237,8 +237,8 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
7. **Création d'une clé privée OpenID Connect** 7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner le champ
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`). `OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/* 8. *Enjoy \o/*

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.20 on 2025-05-08 19:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('activity', '0006_guest_school'),
]
operations = [
migrations.AlterField(
model_name='guest',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='activity.activity'),
),
]

View File

@ -234,7 +234,7 @@ class Guest(models.Model):
""" """
activity = models.ForeignKey( activity = models.ForeignKey(
Activity, Activity,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='+', related_name='+',
) )

View File

@ -95,23 +95,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON);
}); });
}); });
$("#delete_activity").click(function () {
if (!confirm("{% trans 'Are you sure you want to delete this activity?' %}")) {
return;
}
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "DELETE",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
}
}).done(function () {
addMsg("{% trans 'Activity deleted' %}", "success");
window.location.href = "/activity/"; // Redirige vers la liste des activités
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -70,10 +70,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if ".change_"|has_perm:activity %} {% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a>
{% endif %} {% endif %}
{% if not activity.valid and ".delete_"|has_perm:activity %} {% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-danger btn-sm my-1" id="delete_activity"> {% trans "delete"|capfirst %} </a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started and activity.valid %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -15,5 +15,4 @@ urlpatterns = [
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'), path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'), path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'), path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
path('<int:pk>/delete', views.ActivityDeleteView.as_view(), name='delete_activity'),
] ]

View File

@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -153,34 +153,6 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityDeleteView(View):
"""
Deletes an Activity
"""
def delete(self, request, pk):
try:
activity = Activity.objects.get(pk=pk)
activity.delete()
return JsonResponse({"message": "Activity deleted"})
except Activity.DoesNotExist:
return JsonResponse({"error": "Activity not found"}, status=404)
def dispatch(self, *args, **kwargs):
"""
Don't display the delete button if the user has no right to delete.
"""
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"])
if not PermissionBackend.check_perm(self.request, "activity.delete_activity", activity):
raise PermissionDenied(_("You are not allowed to delete this activity."))
if activity.valid:
raise PermissionDenied(_("This activity is valid."))
return super().dispatch(*args, **kwargs)
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm` Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`

View File

@ -169,8 +169,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html" template_name = "food/food_update.html"
def get_sample_object(self): def get_sample_object(self):
# We choose a club which may work or BDE else return BasicFood(
food = BasicFood(
name="", name="",
owner_id=1, owner_id=1,
expiry_date=timezone.now(), expiry_date=timezone.now(),
@ -179,14 +178,6 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_type='DLC', date_type='DLC',
) )
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
return food
return food
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0: if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
@ -237,22 +228,13 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html" template_name = "food/food_update.html"
def get_sample_object(self): def get_sample_object(self):
# We choose a club which may work or BDE else return TransformedFood(
food = TransformedFood(
name="", name="",
owner_id=1, owner_id=1,
expiry_date=timezone.now(), expiry_date=timezone.now(),
is_ready=True, is_ready=True,
) )
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
return food
return food
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.expiry_date = timezone.now() + timedelta(days=3) form.instance.expiry_date = timezone.now() + timedelta(days=3)
@ -264,10 +246,10 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
MAX_FORMS = 100 MAX_FORMS = 10
class ManageIngredientsView(LoginRequiredMixin, UpdateView): class ManageIngredientsView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
A view to manage ingredient for a transformed food A view to manage ingredient for a transformed food
""" """
@ -298,14 +280,6 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
ingredient.end_of_life = _('Fully used in {meal}'.format( ingredient.end_of_life = _('Fully used in {meal}'.format(
meal=self.object.name)) meal=self.object.name))
ingredient.save() ingredient.save()
# We recalculate new expiry date and allergens
self.object.expiry_date = self.object.creation_date + self.object.shelf_life
self.object.allergens.clear()
for ingredient in self.object.ingredients.iterator():
if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens) self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())

View File

@ -1,46 +0,0 @@
from django.db import migrations
def create_bda(apps, schema_editor):
"""
The club BDA is now pre-injected.
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
Club.objects.get_or_create(
id=10,
name="BDA",
email="bda.ensparissaclay@gmail.com",
require_memberships=True,
membership_fee_paid=750,
membership_fee_unpaid=750,
membership_duration=396,
membership_start="2024-08-01",
membership_end="2025-09-30",
)
NoteClub.objects.get_or_create(
id=1937,
club_id=10,
polymorphic_ctype_id=polymorphic_ctype_id,
)
Alias.objects.get_or_create(
id=1937,
note_id=1937,
name="BDA",
normalized_name="bda",
)
class Migration(migrations.Migration):
dependencies = [
('member', '0013_auto_20240801_1436'),
]
operations = [
migrations.RunPython(create_bda),
]

View File

@ -4091,8 +4091,8 @@
158, 158,
159, 159,
160, 160,
212, 212,
222 222
] ]
} }
}, },
@ -4133,14 +4133,14 @@
50, 50,
141, 141,
169, 169,
217, 217,
218, 218,
219, 219,
220, 220,
221, 221,
247, 247,
258, 258,
259 259
] ]
} }
}, },
@ -4152,8 +4152,8 @@
"name": "Pr\u00e9sident\u22c5e de club", "name": "Pr\u00e9sident\u22c5e de club",
"permissions": [ "permissions": [
62, 62,
135, 142,
142 135
] ]
} }
}, },
@ -4538,8 +4538,8 @@
"name": "GC anti-VSS", "name": "GC anti-VSS",
"permissions": [ "permissions": [
42, 42,
135, 135,
150, 150,
163, 163,
164 164
] ]
@ -4555,140 +4555,13 @@
137, 137,
211, 211,
212, 212,
213, 213,
214, 214,
215, 215,
216 216
] ]
} }
}, },
{
"model": "permission.role",
"pk": 23,
"fields": {
"for_club": 2,
"name": "Darbonne",
"permissions": [
30,
31,
32
]
}
},
{
"model": "permission.role",
"pk": 24,
"fields": {
"for_club": null,
"name": "Staffeur⋅euse (S&L,Respo Tech,...)",
"permissions": []
}
},
{
"model": "permission.role",
"pk": 25,
"fields": {
"for_club": null,
"name": "Référent⋅e Bus",
"permissions": [
22,
84,
115,
117,
118,
119,
120,
121,
122
]
}
},
{
"model": "permission.role",
"pk": 28,
"fields": {
"for_club": 10,
"name": "Trésorièr⸱e BDA",
"permissions": [
55,
56,
57,
58,
135,
143,
176,
177,
178,
243,
260,
261,
262,
263,
264,
265,
266,
267,
268,
269
]
}
},
{
"model": "permission.role",
"pk": 30,
"fields": {
"for_club": 10,
"name": "Respo sorties",
"permissions": [
49,
62,
141,
241,
242,
243
]
}
},
{
"model": "permission.role",
"pk": 31,
"fields": {
"for_club": 1,
"name": "Respo comm",
"permissions": [
135,
244
]
}
},
{
"model": "permission.role",
"pk": 32,
"fields": {
"for_club": 10,
"name": "Respo comm Art",
"permissions": [
135,
245
]
}
},
{
"model": "permission.role",
"pk": 33,
"fields": {
"for_club": 10,
"name": "Respo Jam",
"permissions": [
247,
250,
251,
252,
253,
254
]
}
},
{ {
"model": "wei.weirole", "model": "wei.weirole",
"pk": 12, "pk": 12,
@ -4723,15 +4596,5 @@
"model": "wei.weirole", "model": "wei.weirole",
"pk": 18, "pk": 18,
"fields": {} "fields": {}
},
{
"model": "wei.weirole",
"pk": 24,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 25,
"fields": {}
} }
] ]

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes from oauth2_provider.scopes import BaseScopes
from member.models import Club from member.models import Club
@ -18,21 +19,27 @@ class PermissionScopes(BaseScopes):
""" """
def get_all_scopes(self): def get_all_scopes(self):
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})" scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()} for p in Permission.objects.all() for club in Club.objects.all()}
scopes['openid'] = "OpenID Connect"
return scopes
def get_available_scopes(self, application=None, request=None, *args, **kwargs): def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
return [f"{p.id}_{p.membership.club.id}" scopes = [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])] for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes.append('openid')
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs): def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
return [f"{p.id}_{p.membership.club.id}" scopes = [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')] for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
class PermissionOAuth2Validator(OAuth2Validator): class PermissionOAuth2Validator(OAuth2Validator):
@ -49,6 +56,10 @@ class PermissionOAuth2Validator(OAuth2Validator):
"email": request.user.email, "email": request.user.email,
} }
def get_discovery_claims(self, request):
claims = super().get_discovery_claims(self)
return claims + ["name", "normalized_name", "email"]
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
""" """
User can request as many scope as he wants, including invalid scopes, User can request as many scope as he wants, including invalid scopes,
@ -66,6 +77,8 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes: if scope in scopes:
valid_scopes.add(scope) valid_scopes.add(scope)
request.scopes = valid_scopes if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes return valid_scopes

View File

@ -19,6 +19,7 @@ EXCLUDED = [
'oauth2_provider.accesstoken', 'oauth2_provider.accesstoken',
'oauth2_provider.grant', 'oauth2_provider.grant',
'oauth2_provider.refreshtoken', 'oauth2_provider.refreshtoken',
'oauth2_provider.idtoken',
'sessions.session', 'sessions.session',
] ]

View File

@ -171,7 +171,7 @@ class ScopesView(LoginRequiredMixin, TemplateView):
available_scopes = scopes.get_available_scopes(app) available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict() context["scopes"][app] = OrderedDict()
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes] items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0]))) # items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
for k, v in items: for k, v in items:
context["scopes"][app][k] = v context["scopes"][app][k] = v

View File

@ -136,7 +136,7 @@ de diffusion utiles.
Faîtes attention, donc où la sortie est stockée. Faîtes attention, donc où la sortie est stockée.
Il prend 4 options : Il prend 2 options :
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``, * ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es ``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es
@ -149,10 +149,7 @@ Il prend 4 options :
pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe
laquelle des ``n+1`` dernières années. laquelle des ``n+1`` dernières années.
* ``--email``, qui prend en argument une chaine de caractère contenant une adresse email. Le script sort sur la sortie standard la liste des adresses mails à inscrire.
Si aucun email n'est renseigné, le script sort sur la sortie standard la liste des adresses mails à inscrire.
Dans le cas contraire, la liste est envoyée à l'adresse passée en argument.
Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est
malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives. malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives.

View File

@ -270,7 +270,7 @@ OAUTH2_PROVIDER = {
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
'OIDC_ENABLED': True, 'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': 'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'), os.getenv('OIDC_RSA_PRIVATE_KEY', 'CHANGE_ME_IN_ENV_SETTINGS').replace('\\n', '\n'), # for multilines
'SCOPES': { 'openid': "OpenID Connect scope" }, 'SCOPES': { 'openid': "OpenID Connect scope" },
} }