1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-01 13:31:16 +02:00

Compare commits

..

26 Commits

Author SHA1 Message Date
3eed93e346 Remove unused file 2021-02-23 23:38:54 +01:00
4da523a1ba Merge branch 'faster_ci' of https://gitlab.crans.org/bde/nk20 into faster_ci 2021-02-23 23:38:26 +01:00
e74ff54468 please use the configuration I have written for hadolint 2021-02-23 22:23:16 +00:00
2e49c9ffbd Add CI docker linter for CI Dockerfiles 2021-02-23 22:23:16 +00:00
d20a1038a8 Add CI docker linter for nk20 Dockerfile 2021-02-23 22:23:16 +00:00
f6b711bb1b Add hadolint configuration file 2021-02-23 22:23:16 +00:00
893d87a9e1 Add ansible linting to the CI 2021-02-23 22:23:16 +00:00
9f3323c73e Add docker image for ansible lint to be used in CI 2021-02-23 22:23:16 +00:00
c57f81b920 Add skip list for ansible-lint 2021-02-23 22:23:16 +00:00
0636d84286 Add docker image for tox linting to be used in CI 2021-02-23 22:23:16 +00:00
ed06901fae fix typo (added image: twice) 2021-02-23 22:23:16 +00:00
28932f316b copy paste is a bad practice 2021-02-23 22:23:16 +00:00
9b50ba722c Add custom pre-built docker images to be used for the CI 2021-02-23 22:23:16 +00:00
3e3e61d23f Use prebuilt docker images in the CI 2021-02-23 22:23:16 +00:00
1129815ca3 please use the configuration I have written for hadolint 2021-02-23 23:22:51 +01:00
c13172d3ff Add CI docker linter for CI Dockerfiles 2021-02-23 23:14:35 +01:00
fcc4121225 Add CI docker linter for nk20 Dockerfile 2021-02-23 23:14:00 +01:00
a06f355559 Add hadolint configuration file 2021-02-23 23:10:30 +01:00
08df5fcccd Add ansible linting to the CI 2021-02-23 23:02:51 +01:00
b6c0f9758d Add docker image for ansible lint to be used in CI 2021-02-23 23:02:29 +01:00
a23093851f Add skip list for ansible-lint 2021-02-23 22:57:33 +01:00
d803ab5ec2 Add docker image for tox linting to be used in CI 2021-02-22 00:17:49 +01:00
d7a537b6b5 fix typo (added image: twice) 2021-02-21 23:52:42 +01:00
0941ee954d copy paste is a bad practice 2021-02-21 23:46:20 +01:00
fd11d96d95 Add custom pre-built docker images to be used for the CI 2021-02-21 23:40:03 +01:00
4bfc057454 Use prebuilt docker images in the CI 2021-02-21 23:39:08 +01:00
207 changed files with 1912 additions and 5679 deletions

3
.ansible-lint Normal file
View File

@ -0,0 +1,3 @@
skip_list:
- command-instead-of-shell # Use shell only when shell functionality is required
- experimental # all rules tagged as experimental

View File

@ -10,6 +10,7 @@ DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost
DOMAIN=localhost
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost

View File

@ -10,50 +10,22 @@ variables:
# Debian Buster
py37-django22:
stage: test
image: debian:buster-backports
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
image: otthorn/nk20_ci_37
script: tox -e py37-django22
# Ubuntu 20.04
py38-django22:
stage: test
image: ubuntu:20.04
before_script:
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
image: otthorn/nk20_ci_38
script: tox -e py38-django22
# Debian Bullseye
py39-django22:
stage: test
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
image: otthorn/nk20_ci_39
script: tox -e py39-django22
# Tox linter
linters:
stage: quality-assurance
image: debian:buster-backports
@ -64,6 +36,20 @@ linters:
# Be nice to new contributors, but please use `tox`
allow_failure: true
# Ansible linter
ansible-linter:
stage: quality-assurance
image: otthorn/nk20_ci_ansiblelint
script: ansible-lint ansible/
# Docker linter
docker-linter:
stage: quality-assurance
image: hadolint/hadolint
script:
- hadolint -c .hadolint Dockerfile
- hadolint -c .hadolint docker_ci/Dockerfile.*
# Compile documentation
documentation:
stage: docs

4
.hadolint Normal file
View File

@ -0,0 +1,4 @@
ignored:
- DL3008 # Do not force to pin version in apt (Debian)
- DL3013 # Do not force to pin version in pip (PyPI)
- DL3018 # Do not force to pin version in apk (Alpine)

View File

@ -279,8 +279,7 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
**Commentez votre code !**
La documentation plus haut niveau sur le développement et sur l'utilisation
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
## FAQ

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
from member.models import Club
from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .models import Activity, Guest
@ -24,7 +24,7 @@ class ActivityForm(forms.ModelForm):
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
clubs = list(Club.objects.filter(PermissionBackend
.filter_queryset(get_current_request(), Club, "view")).all())
.filter_queryset(get_current_authenticated_user(), Club, "view")).all())
shuffle(clubs)
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone
from django.utils.html import format_html

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5
@ -74,12 +74,12 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
prefix='upcoming-',
)
started_activities = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.filter(open=True, valid=True).all()
context["started_activities"] = started_activities
@ -98,7 +98,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data()
table = GuestTable(data=Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
context["guests"] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
@ -144,7 +144,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
form.fields["inviter"].initial = self.request.user.note
return form
@ -152,7 +152,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
@transaction.atomic
def form_valid(self, form):
form.instance.activity = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
return super().form_valid(form)
def get_success_url(self, **kwargs):
@ -173,7 +173,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
activity = Activity.objects.get(pk=self.kwargs["pk"])
sample_entry = Entry(activity=activity, note=self.request.user.note)
if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
if not activity.activity_type.manage_entries:
@ -191,7 +191,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(activity=activity)\
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
.order_by('last_name', 'first_name').distinct()
if "search" in self.request.GET and self.request.GET["search"]:
@ -230,7 +230,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
)
# Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
@ -256,7 +256,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
@ -281,9 +281,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request,
if PermissionBackend.check_perm(self.request.user,
"activity.add_entry",
Entry(activity=a, note=self.request.user.note,))]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,17 +1,13 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer
from note.api.serializers import NoteSerializer
from note.models import Alias
from rest_framework.serializers import ModelSerializer
class UserSerializer(serializers.ModelSerializer):
class UserSerializer(ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -26,7 +22,7 @@ class UserSerializer(serializers.ModelSerializer):
)
class ContentTypeSerializer(serializers.ModelSerializer):
class ContentTypeSerializer(ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -35,42 +31,3 @@ class ContentTypeSerializer(serializers.ModelSerializer):
class Meta:
model = ContentType
fields = '__all__'
class OAuthSerializer(serializers.ModelSerializer):
"""
Informations that are transmitted by OAuth.
For now, this includes user, profile and valid memberships.
This should be better managed later.
"""
normalized_name = serializers.SerializerMethodField()
profile = ProfileSerializer()
note = NoteSerializer()
memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj):
return Alias.normalize(obj.username)
def get_memberships(self, obj):
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
class Meta:
model = User
fields = (
'id',
'username',
'normalized_name',
'first_name',
'last_name',
'email',
'is_superuser',
'is_active',
'is_staff',
'profile',
'note',
'memberships',
)

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from datetime import datetime, date
from decimal import Decimal
from urllib.parse import quote_plus
from warnings import warn
@ -153,8 +152,6 @@ class TestAPI(TestCase):
value = value.isoformat()
elif isinstance(value, ImageFieldFile):
value = value.name
elif isinstance(value, Decimal):
value = str(value)
query = json.dumps({field.name: value})
# Create sample permission

View File

@ -1,11 +1,10 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.conf.urls import url, include
from rest_framework import routers
from .views import UserInformationView
from .viewsets import ContentTypeViewSet, UserViewSet
# Routers provide an easy way of automatically determining the URL conf.
@ -48,6 +47,5 @@ app_name = 'api'
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^me/', UserInformationView.as_view()),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

View File

@ -1,20 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from rest_framework.generics import RetrieveAPIView
from .serializers import OAuthSerializer
class UserInformationView(RetrieveAPIView):
"""
These fields are give to OAuth authenticators.
"""
serializer_class = OAuthSerializer
def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)
def get_object(self):
return self.request.user

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
@ -9,6 +9,7 @@ from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session
from note.models import Alias
from .serializers import UserSerializer, ContentTypeSerializer
@ -24,7 +25,9 @@ class ReadProtectedModelViewSet(ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
@ -37,7 +40,9 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class UserViewSet(ReadProtectedModelViewSet):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import ModelSerializer
from note.models import NoteUser, Alias
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
from .models import Changelog
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
previous = instance._previous
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request()
user, ip = get_current_authenticated_user(), get_current_ip()
if request is None:
if user is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -71,23 +71,9 @@ def save_object(sender, instance, **kwargs):
# else:
if note.exists():
user = note.get().user
else:
user = None
else:
user = request.user
if 'HTTP_X_REAL_IP' in request.META:
ip = request.META.get('HTTP_X_REAL_IP')
elif 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
else:
ip = request.META.get('REMOTE_ADDR')
if not user.is_authenticated:
# For registration and OAuth2 purposes
user = None
# noinspection PyProtectedMember
if request is not None and instance._meta.label_lower == "auth.user" and previous:
if user is not None and instance._meta.label_lower == "auth.user" and previous:
# On n'enregistre pas les connexions
if instance.last_login != previous.last_login:
return
@ -135,9 +121,9 @@ def delete_object(sender, instance, **kwargs):
return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request()
user, ip = get_current_authenticated_user(), get_current_ip()
if request is None:
if user is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -149,20 +135,6 @@ def delete_object(sender, instance, **kwargs):
# else:
if note.exists():
user = note.get().user
else:
user = None
else:
user = request.user
if 'HTTP_X_REAL_IP' in request.META:
ip = request.META.get('HTTP_X_REAL_IP')
elif 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
else:
ip = request.META.get('REMOTE_ADDR')
if not user.is_authenticated:
# For registration and OAuth2 purposes
user = None
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
class CustomSerializer(ModelSerializer):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,17 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
from note.models import Alias
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["normalized_name"] = Alias.normalize(self.user.username)
return d

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import io

View File

@ -1,14 +1,12 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib
from collections import OrderedDict
from django.conf import settings
from django.contrib.auth.hashers import PBKDF2PasswordHasher, mask_hash
from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
class CustomNK15Hasher(PBKDF2PasswordHasher):
@ -26,22 +24,16 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
def must_update(self, encoded):
if settings.DEBUG:
# Small hack to let superusers to impersonate people.
# Don't change their password.
request = get_current_request()
current_user = request.user
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser:
return False
return True
def verify(self, password, encoded):
if settings.DEBUG:
# Small hack to let superusers to impersonate people.
# If a superuser is already connected, let him/her log in as another person.
request = get_current_request()
current_user = request.user
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser\
and request.session.get("permission_mask", -1) >= 42:
and get_current_session().get("permission_mask", -1) >= 42:
return True
if '|' in encoded:
@ -49,18 +41,6 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
return super().verify(password, encoded)
def safe_summary(self, encoded):
# Displayed information in Django Admin.
if '|' in encoded:
salt, db_hashed_pass = encoded.split('$')[2].split('|')
return OrderedDict([
(_('algorithm'), 'custom_nk15'),
(_('iterations'), '1'),
(_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(db_hashed_pass)),
])
return super().safe_summary(encoded)
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
"""
@ -71,11 +51,8 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
def verify(self, password, encoded):
if settings.DEBUG:
# Small hack to let superusers to impersonate people.
# If a superuser is already connected, let him/her log in as another person.
request = get_current_request()
current_user = request.user
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser\
and request.session.get("permission_mask", -1) >= 42:
and get_current_session().get("permission_mask", -1) >= 42:
return True
return super().verify(password, encoded)

View File

@ -1,23 +0,0 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0006_create_note_account_bde_membership'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='roles',
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
),
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
@ -57,7 +57,7 @@ class Profile(models.Model):
('A1', _("Mathematics (A1)")),
('A2', _("Physics (A2)")),
("A'2", _("Applied physics (A'2)")),
("A''2", _("Chemistry (A''2)")),
('A''2', _("Chemistry (A''2)")),
('A3', _("Biology (A3)")),
('B1234', _("SAPHIRE (B1234)")),
('B1', _("Mechanics (B1)")),
@ -74,7 +74,7 @@ class Profile(models.Model):
promotion = models.PositiveSmallIntegerField(
null=True,
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
default=datetime.date.today().year,
verbose_name=_("promotion"),
help_text=_("Year of entry to the school (None if not ENS student)"),
)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.utils.html import format_html
from note.templatetags.pretty_money import pretty_money
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .models import Club, Membership
@ -51,19 +51,19 @@ class UserTable(tables.Table):
def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail
# Replace also the URL
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
value = ""
record.email = value
return value
def render_section(self, record, value):
return value \
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
else ""
def render_balance(self, record, value):
return pretty_money(value)\
if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else ""
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
class Meta:
attrs = {
@ -93,7 +93,7 @@ class MembershipTable(tables.Table):
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
@ -102,7 +102,7 @@ class MembershipTable(tables.Table):
def render_club(self, value):
# If the user has the right, link the displayed club with the page of its detail.
s = value.name
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
@ -127,7 +127,7 @@ class MembershipTable(tables.Table):
date_end=date.today(),
fee=0,
)
if PermissionBackend.check_perm(get_current_request(),
if PermissionBackend.check_perm(get_current_authenticated_user(),
"member.add_membership", empty_membership): # If the user has right
renew_url = reverse_lazy('member:club_renew_membership',
kwargs={"pk": record.pk})
@ -142,7 +142,7 @@ class MembershipTable(tables.Table):
# If the user has the right to manage the roles, display the link to manage them
roles = record.roles.all()
s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+ "'>" + s + "</a>")
return s
@ -165,7 +165,7 @@ class ClubManagerTable(tables.Table):
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)

View File

@ -39,13 +39,13 @@
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %}
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %}
{% endif %}
</dl>

View File

@ -5,26 +5,17 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %}
{% block content %}
<div class="row mt-4">
<div class="col-xl-6">
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Token authentication" %}</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<div class="alert alert-info">
<h4>À quoi sert un jeton d'authentification ?</h4>
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
depuis un client externe.<br />
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code>
pour pouvoir vous identifier.<br /><br />
La documentation de l'API est disponible ici :
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
</div>
Une documentation de l'API arrivera ultérieurement.
</div>
<div class="alert alert-info">
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
@ -33,70 +24,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
<br />
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
</div>
<div class="alert alert-warning">
<strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
</div>
</div>
<div class="card-footer text-center">
<a href="?regenerate">
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
</a>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card">
<div class="card-header text-center">
<h3>{% trans "OAuth2 authentication" %}</h3>
</div>
<div class="card-header">
<div class="alert alert-info">
<p>
La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
permettre à des applications tierces d'interagir avec la Note en récoltant des informations
(de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
</p>
<p>
L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
les droits dont on a besoin, en restreignant leur usage par jeton généré.
</p>
<p>
La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
<a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
</p>
</div>
Liste des URL à communiquer à votre application :
<ul>
<li>
{% trans "Authorization:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
</li>
<li>
{% trans "Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
</li>
<li>
{% trans "Revoke Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
</li>
<li>
{% trans "Introspect Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
</li>
</ul>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
</div>
</div>
</div>
</div>
<div class="alert alert-warning">
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
</div>
<a href="?regenerate">
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
</a>
{% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth.models import User

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta, date
@ -21,7 +21,7 @@ from rest_framework.authtoken.models import Token
from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable
from note_kfet.middlewares import _set_current_request
from note_kfet.middlewares import _set_current_user_and_ip
from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -41,8 +41,7 @@ class CustomLoginView(LoginView):
@transaction.atomic
def form_valid(self, form):
logout(self.request)
self.request.user = form.get_user()
_set_current_request(self.request)
_set_current_user_and_ip(form.get_user(), self.request.session, None)
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form)
@ -71,7 +70,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency:
@ -154,13 +153,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at")\
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("club__name", "-date_start")
# Display only the most recent membership
club_list = club_list.distinct("club__name")\
@ -177,20 +176,21 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note.is_active = True
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
.check_perm(self.request.user, "note.change_noteuser_is_active",
modified_note)
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_note_is_active", modified_note)
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_note_is_active", modified_note)
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
return context
@ -237,7 +237,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
.filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists()
return context
@ -256,8 +256,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
normalized_name="",
@ -382,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
name="",
email="club@example.com",
))
@ -404,7 +404,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs)
club = context["club"]
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
# managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
@ -413,7 +413,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.order_by('-created_at')
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
@ -422,7 +422,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club_member = Membership.objects.filter(
club=club,
date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("user__username", "-date_start")
# Display only the most recent membership
club_member = club_member.distinct("user__username")\
@ -459,8 +459,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
normalized_name="",
@ -535,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form = context['form']
if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid
# Ensure that the user is member of the parent club and all its the family tree.
@ -625,6 +625,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# Retrieve form data
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
if not credit_type:
@ -655,7 +658,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user,
club=club.parent_club,
date_start__gte=club.parent_club.membership_start,
date_start__lte=timezone.now(),
date_end__gte=club.parent_club.membership_end,
).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True
@ -670,8 +674,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
.format(form.instance.club.membership_end))
error = True
if credit_amount and not SpecialTransaction.validate_payment_form(form):
# Check that special information for payment are filled
if credit_amount:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
error = True
if not first_name:
form.add_error('first_name', _("This field is required."))
error = True
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
error = True
return not error
@ -683,7 +695,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
"""
# Get the club that is concerned by the membership
if "club_pk" in self.kwargs: # get from url of new membership
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
old_membership = None
@ -734,7 +746,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# When we renew the BDE membership, we update the profile section
# that should happens at least once a year.
user.profile.section = user.profile.section_generated
user.profile._force_save = True
user.profile.save()
# Credit note before the membership is created.
@ -867,7 +878,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = Club.objects.filter(
PermissionBackend.filter_queryset(self.request, Club, "view")
PermissionBackend.filter_queryset(self.request.user, Club, "view")
).get(pk=self.kwargs["pk"])
context["club"] = club

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer
from member.api.serializers import MembershipSerializer
from member.models import Membership
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
@ -126,7 +126,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
"""
# If the user has no right to see the note, then we only display the note identifier
return NotePolymorphicSerializer().to_representation(obj.note)\
if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
else dict(
id=obj.note.id,
name=str(obj.note),
@ -142,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
def get_membership(self, obj):
if isinstance(obj.note, NoteUser):
memberships = Membership.objects.filter(
PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
user=obj.note.user,
club=2, # Kfet
).order_by("-date_start")

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \

View File

@ -1,6 +1,5 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.conf import settings
from django.db.models import Q
@ -11,6 +10,7 @@ from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from note_kfet.middlewares import get_current_session
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
@ -40,11 +40,12 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
Parse query and apply filters.
:return: The filtered set of requested notes
"""
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
| PermissionBackend.filter_queryset(self.request, NoteUser, "view")
| PermissionBackend.filter_queryset(self.request, NoteClub, "view")
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
.distinct()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
| PermissionBackend.filter_queryset(user, NoteUser, "view")
| PermissionBackend.filter_queryset(user, NoteClub, "view")
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
@ -66,15 +67,14 @@ class AliasViewSet(ReadProtectedModelViewSet):
serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ]
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# alias owner cannot be change once establish
serializer_class.Meta.read_only_fields = ('note',)
setattr(serializer_class.Meta, 'read_only_fields', ('note',))
return serializer_class
def destroy(self, request, *args, **kwargs):
@ -82,7 +82,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
def get_queryset(self):
@ -118,8 +118,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ]
def get_queryset(self):
@ -134,31 +133,23 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
alias = self.request.query_params.get("alias", None)
# Check if this is a valid regex. If not, we won't check regex
try:
re.compile(alias)
valid_regex = True
except (re.error, TypeError):
valid_regex = False
suffix = '__iregex' if valid_regex else '__istartswith'
alias_prefix = '^' if valid_regex else ''
queryset = queryset.prefetch_related('note')
if alias:
# We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias.
queryset = queryset.filter(
**{f'name{suffix}': alias_prefix + alias}
name__iregex="^" + alias
).union(
queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True).union(
queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
Q(normalized_name__iregex="^" + alias.lower())
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True)
@ -214,5 +205,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
ordering_fields = ['created_at', 'amount', ]
def get_queryset(self):
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
.order_by("created_at", "id")

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime

View File

@ -1,19 +0,0 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0004_remove_null_tag_on_charfields'),
]
operations = [
migrations.AlterField(
model_name='alias',
name='note',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser

View File

@ -1,9 +1,10 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata
from django.conf import settings
from django.conf.global_settings import DEFAULT_FROM_EMAIL
from django.core.exceptions import ValidationError
from django.core.mail import send_mail
from django.core.validators import RegexValidator
@ -189,8 +190,8 @@ class NoteClub(Note):
def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text,
settings.DEFAULT_FROM_EMAIL, [self.club.email], html_message=html)
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, DEFAULT_FROM_EMAIL,
[self.club.email], html_message=html)
class NoteSpecial(Note):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import ValidationError
@ -333,36 +333,6 @@ class SpecialTransaction(Transaction):
self.clean()
super().save(*args, **kwargs)
@staticmethod
def validate_payment_form(form):
"""
Ensure that last name and first name are filled for a form that creates a SpecialTransaction,
and check that if the user pays with a check, then the bank field is filled.
Return True iff there is no error.
Whenever there is an error, they are inserted in the form errors.
"""
credit_type = form.cleaned_data["credit_type"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
error = False
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
error = True
if not first_name:
form.add_error('first_name', _("This field is required."))
error = True
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
error = True
return not error
class Meta:
verbose_name = _("Special transaction")
verbose_name_plural = _("Special transactions")

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone

View File

@ -1,4 +1,4 @@
// Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks.
@ -28,7 +28,7 @@ $(document).ready(function () {
// Switching in double consumptions mode should update the layout
$('#double_conso').change(function () {
document.getElementById('consos_list_div').classList.remove('d-none')
$('#consos_list_div').removeClass('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
const note_list_obj = $('#note_list')
@ -37,7 +37,7 @@ $(document).ready(function () {
note_list_obj.html('')
buttons.forEach(function (button) {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
$('#conso_button_' + button.id).click(function () {
if (LOCK) { return }
removeNote(button, 'conso_button', buttons, 'consos_list')()
})
@ -46,7 +46,7 @@ $(document).ready(function () {
})
$('#single_conso').change(function () {
document.getElementById('consos_list_div').classList.add('d-none')
$('#consos_list_div').addClass('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
const consos_list_obj = $('#consos_list')
@ -68,9 +68,9 @@ $(document).ready(function () {
})
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
document.querySelector("label[for='double_conso']").classList.remove('active')
$("label[for='double_conso']").removeClass('active')
document.getElementById("consume_all").addEventListener('click', consumeAll)
$('#consume_all').click(consumeAll)
})
notes = []
@ -127,10 +127,11 @@ function addConso (dest, amount, type, category_id, category_name, template_id,
html += li('conso_button_' + button.id, button.name +
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
})
document.getElementById(list).innerHTML = html
buttons.forEach((button) => {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
$('#' + list).html(html)
buttons.forEach(function (button) {
$('#conso_button_' + button.id).click(function () {
if (LOCK) { return }
removeNote(button, 'conso_button', buttons, list)()
})
@ -145,13 +146,12 @@ function reset () {
notes_display.length = 0
notes.length = 0
buttons.length = 0
document.getElementById('note_list').innerHTML = ''
document.getElementById('consos_list').innerHTML = ''
document.getElementById('note').value = ''
document.getElementById('note').dataset.originTitle = ''
$('#note').tooltip('hide')
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
document.getElementById('profile_pic_link').href = '#'
$('#note_list').html('')
$('#consos_list').html('')
$('#note').val('')
$('#note').attr('data-original-title', '').tooltip('hide')
$('#profile_pic').attr('src', '/static/member/img/default_picture.png')
$('#profile_pic_link').attr('href', '#')
refreshHistory()
refreshBalance()
LOCK = false
@ -168,7 +168,7 @@ function consumeAll () {
let error = false
if (notes_display.length === 0) {
document.getElementById('note').classList.add('is-invalid')
$('#note').addClass('is-invalid')
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
error = true
}

View File

@ -243,7 +243,7 @@ $('#btn_transfer').click(function () {
error = true
}
const amount = Math.round(100 * amount_field.val())
const amount = Math.floor(100 * amount_field.val())
if (amount > 2147483647) {
amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import html
@ -7,7 +7,7 @@ import django_tables2 as tables
from django.utils.html import format_html
from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .models.notes import Alias
@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
"class": lambda record:
str(record.valid).lower()
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
else ''),
"data-toggle": "tooltip",
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
if PermissionBackend.check_perm(get_current_request(),
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None,
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
+ ', "' + str(record.__class__.__name__) + '")'
if PermissionBackend.check_perm(get_current_request(),
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None,
"onmouseover": lambda record: '$("#invalidity_reason_'
@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
"""
has_perm = PermissionBackend \
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
val = "" if value else ""
@ -165,7 +165,7 @@ class AliasTable(tables.Table):
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': lambda record: 'col-sm-1' + (
' d-none' if not PermissionBackend.check_perm(
get_current_request(), "note.delete_alias",
get_current_authenticated_user(), "note.delete_alias",
record) else '')}}, verbose_name=_("Delete"), )

View File

@ -57,7 +57,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<select id="credit_type" class="form-control custom-select d-none">
<select id="credit_type" class="custom-select d-none">
{% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %}
@ -84,7 +84,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ul class="list-group list-group-flush" id="dest_note_list">
</ul>
<div class="card-body">
<select id="debit_type" class="form-control custom-select d-none">
<select id="debit_type" class="custom-select d-none">
{% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
@ -38,7 +38,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
def get_queryset(self, **kwargs):
# retrieves only Transaction that user has the right to see.
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request, Transaction, "view")
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-created_at").all()[:20]
def get_context_data(self, **kwargs):
@ -47,16 +47,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
context['special_types'] = NoteSpecial.objects\
.filter(PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
.order_by("special_type").all()
# Add a shortcut for entry page for open activities
if "activity" in settings.INSTALLED_APPS:
from activity.models import Activity
activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request,
if PermissionBackend.check_perm(self.request.user,
"activity.add_entry",
Entry(activity=a,
note=self.request.user.note, ))]
@ -159,7 +159,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return self.handle_no_permission()
templates = TransactionTemplate.objects.filter(
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
)
if not templates.exists():
raise PermissionDenied(_("You can't see any button."))
@ -170,7 +170,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
restrict to the transaction history the user can see.
"""
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request, Transaction, "view")
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-created_at").all()[:20]
def get_context_data(self, **kwargs):
@ -180,13 +180,13 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
# for each category, find which transaction templates the user can see.
for category in categories:
category.templates_filtered = category.templates.filter(
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).filter(display=True).order_by('name').all()
context['categories'] = [cat for cat in categories if cat.templates_filtered]
# some transactiontemplate are put forward to find them easily
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).order_by('name').all()
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
@ -209,7 +209,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
data = form.cleaned_data if form.is_valid() else {}
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
if "source" in data and data["source"]:

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'permission.apps.PermissionConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import PermissionViewSet, RoleViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadOnlyProtectedModelViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,15 +1,15 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, F
from django.utils import timezone
from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_session
from member.models import Membership, Club
from .decorators import memoize
@ -26,31 +26,14 @@ class PermissionBackend(ModelBackend):
@staticmethod
@memoize
def get_raw_permissions(request, t):
def get_raw_permissions(user, t):
"""
Query permissions of a certain type for a user, then memoize it.
:param request: The current request
:param user: The owner of the permissions
:param t: The type of the permissions: view, change, add or delete
:return: The queryset of the permissions of the user (memoized) grouped by clubs
"""
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
def permission_filter(membership_obj):
query = Q(pk=-1)
for scope in request.auth.scope.split(' '):
permission_id, club_id = scope.split('_')
if int(club_id) == membership_obj.club_id:
query |= Q(pk=permission_id)
return query
else:
user = request.user
def permission_filter(membership_obj):
return Q(mask__rank__lte=request.session.get("permission_mask", 42))
if user.is_anonymous:
if isinstance(user, AnonymousUser):
# Unauthenticated users have no permissions
return Permission.objects.none()
@ -60,7 +43,7 @@ class PermissionBackend(ModelBackend):
for membership in memberships:
for role in membership.roles.all():
for perm in role.permissions.filter(permission_filter(membership), type=t).all():
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
if not perm.permanent:
if membership.date_start > date.today() or membership.date_end < date.today():
continue
@ -69,22 +52,16 @@ class PermissionBackend(ModelBackend):
return perms
@staticmethod
def permissions(request, model, type):
def permissions(user, model, type):
"""
List all permissions of the given user that applies to a given model and a give type
:param request: The current request
:param user: The owner of the permissions
:param model: The model that the permissions shoud apply
:param type: The type of the permissions: view, change, add or delete
:return: A generator of the requested permissions
"""
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
else:
user = request.user
for permission in PermissionBackend.get_raw_permissions(request, type):
for permission in PermissionBackend.get_raw_permissions(user, type):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
continue
@ -111,26 +88,20 @@ class PermissionBackend(ModelBackend):
@staticmethod
@memoize
def filter_queryset(request, model, t, field=None):
def filter_queryset(user, model, t, field=None):
"""
Filter a queryset by considering the permissions of a given user.
:param request: The current request
:param user: The owner of the permissions that are fetched
:param model: The concerned model of the queryset
:param t: The type of modification (view, add, change, delete)
:param field: The field of the model to test, if concerned
:return: A query that corresponds to the filter to give to a queryset
"""
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user = request.auth.user
else:
user = request.user
if user is None or user.is_anonymous:
if user is None or isinstance(user, AnonymousUser):
# Anonymous users can't do anything
return Q(pk=-1)
if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
# Superusers have all rights
return Q()
@ -139,7 +110,7 @@ class PermissionBackend(ModelBackend):
# Never satisfied
query = Q(pk=-1)
perms = PermissionBackend.permissions(request, model, t)
perms = PermissionBackend.permissions(user, model, t)
for perm in perms:
if perm.field and field != perm.field:
continue
@ -151,7 +122,7 @@ class PermissionBackend(ModelBackend):
@staticmethod
@memoize
def check_perm(request, perm, obj=None):
def check_perm(user_obj, perm, obj=None):
"""
Check is the given user has the permission over a given object.
The result is then memoized.
@ -159,18 +130,11 @@ class PermissionBackend(ModelBackend):
primary key, the result is not memoized. Moreover, the right could change
(e.g. for a transaction, the balance of the user could change)
"""
# Requested by a shell
if request is None:
if user_obj is None or isinstance(user_obj, AnonymousUser):
return False
user_obj = request.user
sess = request.session
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user_obj = request.auth.user
if user_obj is None or user_obj.is_anonymous:
sess = get_current_session()
if sess is not None and sess.session_key is None:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
@ -185,19 +149,16 @@ class PermissionBackend(ModelBackend):
ct = ContentType.objects.get_for_model(obj)
if any(permission.applies(obj, perm_type, perm_field)
for permission in PermissionBackend.permissions(request, ct, perm_type)):
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
return True
return False
def has_perm(self, user_obj, perm, obj=None):
# Warning: this does not check that user_obj has the permission,
# but if the current request has the permission.
# This function is implemented for backward compatibility, and should not be used.
return PermissionBackend.check_perm(get_current_request(), perm, obj)
return PermissionBackend.check_perm(user_obj, perm, obj)
def has_module_perms(self, user_obj, app_label):
return False
def get_all_permissions(self, user_obj, obj=None):
ct = ContentType.objects.get_for_model(obj)
return list(self.permissions(get_current_request(), ct, "view"))
return list(self.permissions(user_obj, ct, "view"))

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
from functools import lru_cache
from time import time
from django.contrib.sessions.models import Session
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_session
def memoize(f):
@ -48,11 +48,11 @@ def memoize(f):
last_collect = time()
# If there is no session, then we don't memoize anything.
request = get_current_request()
if request is None or request.session is None or request.session.session_key is None:
sess = get_current_session()
if sess is None or sess.session_key is None:
return f(*args, **kwargs)
sess_key = request.session.session_key
sess_key = sess.session_key
if sess_key not in sess_funs:
# lru_cache makes the job of memoization
# We store only the 512 latest data per session. It has to be enough.

View File

@ -627,7 +627,7 @@
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "Voir les personnes qu'on a invitées"
}
},
@ -1235,7 +1235,7 @@
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "Voir le dernier WEI"
}
},
@ -1267,7 +1267,7 @@
"type": "add",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "M'inscrire au dernier WEI"
}
},
@ -1331,7 +1331,7 @@
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "Voir ma propre inscription WEI"
}
},
@ -1379,7 +1379,7 @@
"type": "change",
"mask": 1,
"field": "soge_credit",
"permanent": true,
"permanent": false,
"description": "Indiquer si mon inscription WEI est payée par la Société générale tant qu'elle n'est pas validée"
}
},
@ -1427,7 +1427,7 @@
"type": "change",
"mask": 1,
"field": "birth_date",
"permanent": true,
"permanent": false,
"description": "Modifier la date de naissance de ma propre inscription WEI"
}
},
@ -1459,7 +1459,7 @@
"type": "change",
"mask": 1,
"field": "gender",
"permanent": true,
"permanent": false,
"description": "Modifier le genre de ma propre inscription WEI"
}
},
@ -1491,7 +1491,7 @@
"type": "change",
"mask": 1,
"field": "health_issues",
"permanent": true,
"permanent": false,
"description": "Modifier mes problèmes de santé de mon inscription WEI"
}
},
@ -1523,7 +1523,7 @@
"type": "change",
"mask": 1,
"field": "emergency_contact_name",
"permanent": true,
"permanent": false,
"description": "Modifier le nom du contact en cas d'urgence de mon inscription WEI"
}
},
@ -1555,7 +1555,7 @@
"type": "change",
"mask": 1,
"field": "emergency_contact_phone",
"permanent": true,
"permanent": false,
"description": "Modifier le téléphone du contact en cas d'urgence de mon inscription WEI"
}
},
@ -1699,7 +1699,7 @@
"type": "add",
"mask": 3,
"field": "",
"permanent": true,
"permanent": false,
"description": "Créer une adhésion WEI pour le dernier WEI"
}
},
@ -2003,7 +2003,7 @@
"type": "change",
"mask": 1,
"field": "clothing_cut",
"permanent": true,
"permanent": false,
"description": "Modifier ma coupe de vêtements de mon inscription WEI"
}
},
@ -2035,7 +2035,7 @@
"type": "change",
"mask": 1,
"field": "clothing_size",
"permanent": true,
"permanent": false,
"description": "Modifier la taille de vêtements de mon inscription WEI"
}
},
@ -2243,7 +2243,7 @@
"type": "change",
"mask": 1,
"field": "information_json",
"permanent": true,
"permanent": false,
"description": "Modifier mes préférences en terme de bus et d'équipe si mon inscription n'est pas validée et que je suis en 2A+"
}
},
@ -2883,7 +2883,6 @@
3,
4,
5,
6,
7,
8,
9,
@ -2891,10 +2890,6 @@
11,
12,
13,
14,
15,
16,
17,
22,
48,
52,
@ -2912,6 +2907,11 @@
"for_club": 2,
"name": "Adh\u00e9rent Kfet",
"permissions": [
6,
14,
15,
16,
17,
22,
34,
36,
@ -3304,7 +3304,6 @@
30,
31,
70,
72,
143,
166,
167,
@ -3496,7 +3495,7 @@
"model": "permission.role",
"pk": 20,
"fields": {
"for_club": 1,
"for_club": 2,
"name": "PC Kfet",
"permissions": [
6,
@ -3512,9 +3511,6 @@
56,
57,
58,
70,
72,
135,
137,
143,
147,
@ -3525,7 +3521,8 @@
176,
177,
180,
181
181,
182
]
}
},

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import functools

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.permissions import DjangoObjectPermissions
@ -45,7 +45,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not all(PermissionBackend.check_perm(request, perm, obj) for perm in perms):
if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms):
# If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see
# a 404 response.

View File

@ -1,34 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.scopes import BaseScopes
from member.models import Club
from note_kfet.middlewares import get_current_request
from .backends import PermissionBackend
from .models import Permission
class PermissionScopes(BaseScopes):
"""
An OAuth2 scope is defined by a permission object and a club.
A token will have a subset of permissions from the owner of the application,
and can be useful to make queries through the API with limited privileges.
"""
def get_all_scopes(self):
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()}
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
return [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
@ -16,9 +16,6 @@ EXCLUDED = [
'contenttypes.contenttype',
'logs.changelog',
'migrations.migration',
'oauth2_provider.accesstoken',
'oauth2_provider.grant',
'oauth2_provider.refreshtoken',
'sessions.session',
]
@ -34,8 +31,8 @@ def pre_save_object(sender, instance, **kwargs):
if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"):
return
request = get_current_request()
if request is None:
user = get_current_authenticated_user()
if user is None:
# Action performed on shell is always granted
return
@ -48,7 +45,7 @@ def pre_save_object(sender, instance, **kwargs):
# We check if the user can change the model
# If the user has all right on a model, then OK
if PermissionBackend.check_perm(request, app_label + ".change_" + model_name, instance):
if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance):
return
# In the other case, we check if he/she has the right to change one field
@ -61,14 +58,7 @@ def pre_save_object(sender, instance, **kwargs):
# If the field wasn't modified, no need to check the permissions
if old_value == new_value:
continue
if app_label == 'auth' and model_name == 'user' and field.name == 'password' and request.user.is_anonymous:
# We must ignore password changes from anonymous users since it can be done by people that forgot
# their password. We trust password change form.
continue
if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
instance):
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
raise PermissionDenied(
_("You don't have the permission to change the field {field} on this instance of model"
" {app_label}.{model_name}.")
@ -76,7 +66,7 @@ def pre_save_object(sender, instance, **kwargs):
)
else:
# We check if the user has right to add the object
has_perm = PermissionBackend.check_perm(request, app_label + ".add_" + model_name, instance)
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
if not has_perm:
raise PermissionDenied(
@ -97,8 +87,8 @@ def pre_delete_object(instance, **kwargs):
# Don't check permissions on force-deleted objects
return
request = get_current_request()
if request is None:
user = get_current_authenticated_user()
if user is None:
# Action performed on shell is always granted
return
@ -107,7 +97,7 @@ def pre_delete_object(instance, **kwargs):
model_name = model_name_full[1]
# We check if the user has rights to delete the object
if not PermissionBackend.check_perm(request, app_label + ".delete_" + model_name, instance):
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
raise PermissionDenied(
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
.format(app_label=app_label, model_name=model_name))

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
@ -8,7 +8,7 @@ from django.urls import reverse_lazy
from django.utils.html import format_html
from django_tables2 import A
from member.models import Membership
from note_kfet.middlewares import get_current_request
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
@ -20,7 +20,7 @@ class RightsTable(tables.Table):
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
return s
@ -28,7 +28,7 @@ class RightsTable(tables.Table):
def render_club(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.name
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
@ -42,7 +42,7 @@ class RightsTable(tables.Table):
| Q(name="Bureau de club"))
& Q(weirole__isnull=True))).all()
s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+ "'>" + s + "</a>")
return s

View File

@ -1,74 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h2>{% trans "Available scopes" %}</h2>
</div>
<div class="card-body">
<div class="accordion" id="accordionApps">
{% for app, app_scopes in scopes.items %}
<div class="card">
<div class="card-header" id="app-{{ app.name.lower }}-title">
<a class="text-decoration-none collapsed" href="#" data-toggle="collapse"
data-target="#app-{{ app.name.lower }}" aria-expanded="false"
aria-controls="app-{{ app.name.lower }}">
{{ app.name }}
</a>
</div>
<div class="collapse" id="app-{{ app.name.lower }}" aria-labelledby="app-{{ app.name.lower }}" data-target="#accordionApps">
<div class="card-body">
{% for scope_id, scope_desc in app_scopes.items %}
<div class="form-group">
<label class="form-check-label" for="scope-{{ app.name.lower }}-{{ scope_id }}">
<input type="checkbox" id="scope-{{ app.name.lower }}-{{ scope_id }}"
name="scope-{{ app.name.lower }}" class="checkboxinput form-check-input" value="{{ scope_id }}">
{{ scope_desc }}
</label>
</div>
{% endfor %}
<p id="url-{{ app.name.lower }}">
<a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code" target="_blank">
{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code
</a>
</p>
</div>
</div>
</div>
{% empty %}
<p>
{% trans "No applications defined" %}.
<a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}.
</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
{% for app in scopes.keys %}
let elements = document.getElementsByName("scope-{{ app.name.lower }}");
for (let element of elements) {
element.onchange = function (event) {
let scope = ""
for (let element of elements) {
if (element.checked) {
scope += element.value + " "
}
}
scope = scope.substr(0, scope.length - 1)
document.getElementById("url-{{ app.name.lower }}").innerHTML = 'Scopes : ' + scope
+ '<br><a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='+ scope.replaceAll(' ', '%20')
+ '" target="_blank">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='
+ scope.replaceAll(' ', '%20') + '</a>'
}
}
{% endfor %}
</script>
{% endblock %}

View File

@ -1,12 +1,12 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from django import template
from note_kfet.middlewares import get_current_request
from ..backends import PermissionBackend
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend
@stringfilter
@ -14,10 +14,9 @@ def not_empty_model_list(model_name):
"""
Return True if and only if the current user has right to see any object of the given model.
"""
request = get_current_request()
user = request.user
session = request.session
if user is None or not user.is_authenticated:
user = get_current_authenticated_user()
session = get_current_session()
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
@ -30,12 +29,11 @@ def model_list(model_name, t="view", fetch=True):
"""
Return the queryset of all visible instances of the given model.
"""
request = get_current_request()
user = request.user
user = get_current_authenticated_user()
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(request, ct, t))
if user is None or not user.is_authenticated:
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
if user is None or isinstance(user, AnonymousUser):
return qs.none()
if fetch:
qs = qs.all()
@ -51,7 +49,7 @@ def model_list_length(model_name, t="view"):
def has_perm(perm, obj):
return PermissionBackend.check_perm(get_current_request(), perm, obj)
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
register = template.Library()

View File

@ -1,94 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string
from member.models import Membership, Club
from note.models import NoteUser
from oauth2_provider.models import Application, AccessToken
from ..models import Role, Permission
class OAuth2TestCase(TestCase):
fixtures = ('initial', )
def setUp(self):
self.user = User.objects.create(
username="toto",
)
self.application = Application.objects.create(
name="Test",
client_type=Application.CLIENT_PUBLIC,
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
user=self.user,
)
def test_oauth2_access(self):
"""
Create a simple OAuth2 access token that only has the right to see data of the current user
and check that this token has required access, and nothing more.
"""
bde = Club.objects.get(name="BDE")
view_user_perm = Permission.objects.get(pk=1) # View own user detail
# Create access token that has access to our own user detail
token = AccessToken.objects.create(
user=self.user,
application=self.application,
scope=f"{view_user_perm.pk}_{bde.pk}",
token=get_random_string(64),
expires=timezone.now() + timedelta(days=365),
)
# No access without token
resp = self.client.get(f'/api/user/{self.user.pk}/')
self.assertEqual(resp.status_code, 403)
# Valid token but user has no membership, so the query is not returning the user object
resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
self.assertEqual(resp.status_code, 404)
# Create membership to validate permissions
NoteUser.objects.create(user=self.user)
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
membership.save()
# User is now a member and can now see its own user detail
resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
self.assertEqual(resp.status_code, 200)
# Token is not granted to see profile detail
resp = self.client.get(f'/api/members/profile/{self.user.profile.pk}/',
**{'Authorization': f'Bearer {token.token}'})
self.assertEqual(resp.status_code, 404)
def test_scopes(self):
"""
Ensure that the scopes page is loading.
"""
self.client.force_login(self.user)
resp = self.client.get(reverse('permission:scopes'))
self.assertEqual(resp.status_code, 200)
self.assertIn(self.application, resp.context['scopes'])
self.assertNotIn('1_1', resp.context['scopes'][self.application]) # The user has not this permission
# Create membership to validate permissions
bde = Club.objects.get(name="BDE")
NoteUser.objects.create(user=self.user)
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
membership.save()
resp = self.client.get(reverse('permission:scopes'))
self.assertEqual(resp.status_code, 200)
self.assertIn(self.application, resp.context['scopes'])
self.assertIn('1_1', resp.context['scopes'][self.application]) # Now the user has this permission

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta, date

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User

View File

@ -1,17 +1,10 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.urls import path
from .views import RightsView, ScopesView
from permission.views import RightsView
app_name = 'permission'
urlpatterns = [
path('rights/', RightsView.as_view(), name="rights"),
path('rights', RightsView.as_view(), name="rights"),
]
if "oauth2_provider" in settings.INSTALLED_APPS:
urlpatterns += [
path('scopes/', ScopesView.as_view(), name="scopes"),
]

View File

@ -1,6 +1,6 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from collections import OrderedDict
from datetime import date
from django.contrib.auth.mixins import LoginRequiredMixin
@ -28,7 +28,7 @@ class ProtectQuerysetMixin:
"""
def get_queryset(self, filter_permissions=True, **kwargs):
qs = super().get_queryset(**kwargs)
return qs.filter(PermissionBackend.filter_queryset(self.request, qs.model, "view")).distinct()\
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()\
if filter_permissions else qs
def get_object(self, queryset=None):
@ -53,7 +53,7 @@ class ProtectQuerysetMixin:
# We could also delete the field, but some views might be affected.
meta = form.instance._meta
for key in form.base_fields:
if not PermissionBackend.check_perm(self.request,
if not PermissionBackend.check_perm(self.request.user,
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
form.fields[key].widget = HiddenInput()
@ -101,7 +101,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
# noinspection PyProtectedMember
app_label, model_name = model_class._meta.app_label, model_class._meta.model_name.lower()
perm = app_label + ".add_" + model_name
if not PermissionBackend.check_perm(request, perm, self.get_sample_object()):
if not PermissionBackend.check_perm(request.user, perm, self.get_sample_object()):
raise PermissionDenied(_("You don't have the permission to add an instance of model "
"{app_label}.{model_name}.").format(app_label=app_label, model_name=model_name))
return super().dispatch(request, *args, **kwargs)
@ -143,26 +143,3 @@ class RightsView(TemplateView):
prefix="superusers-")
return context
class ScopesView(LoginRequiredMixin, TemplateView):
template_name = "permission/scopes.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
from oauth2_provider.models import Application
from .scopes import PermissionScopes
scopes = PermissionScopes()
context["scopes"] = {}
all_scopes = scopes.get_all_scopes()
for app in Application.objects.filter(user=self.request.user).all():
available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict()
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])))
for k, v in items:
context["scopes"][app][k] = v
return context

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'registration.apps.RegistrationConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
@ -46,8 +46,7 @@ class SignUpForm(UserCreationForm):
class DeclareSogeAccountOpenedForm(forms.Form):
soge_account = forms.BooleanField(
label=_("I declare that I opened or I will open soon a bank account in the Société générale with the BDE \
partnership."),
label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."),
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
"account, you will have to pay the BDE membership."),
required=False,
@ -102,14 +101,14 @@ class ValidationForm(forms.Form):
required=False,
)
join_bde = forms.BooleanField(
join_BDE = forms.BooleanField(
label=_("Join BDE Club"),
required=False,
initial=True,
)
# The user can join the Kfet club at the inscription
join_kfet = forms.BooleanField(
join_Kfet = forms.BooleanField(
label=_("Join Kfet Club"),
required=False,
initial=True,

View File

@ -1,8 +1,9 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.contrib.auth.models import User
from treasury.models import SogeCredit

View File

@ -100,14 +100,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
bank.attr('disabled', true);
bank.val('Société générale');
let join_bde = $("#id_join_bde");
let join_BDE = $("#id_join_BDE");
join_BDE.attr('disabled', true);
join_BDE.attr('checked', 'checked');
join_bde.attr('disabled', true);
join_bde.attr('checked', 'checked');
let join_kfet = $("#id_join_kfet");
join_kfet.attr('disabled', true);
join_kfet.attr('checked', 'checked');
let join_Kfet = $("#id_join_Kfet");
join_Kfet.attr('disabled', true);
join_Kfet.attr('checked', 'checked');
}
soge_field.change(fillFields);

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
@ -196,8 +196,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=False,
join_kfet=False,
join_BDE=False,
join_Kfet=False,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -210,8 +210,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=False,
join_kfet=True,
join_BDE=False,
join_Kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -224,8 +224,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="J'ai pas d'argent",
join_bde=True,
join_kfet=True,
join_BDE=True,
join_Kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -238,8 +238,8 @@ class TestValidateRegistration(TestCase):
last_name="",
first_name="",
bank="",
join_bde=True,
join_kfet=True,
join_BDE=True,
join_Kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -255,8 +255,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=True,
join_kfet=False,
join_BDE=True,
join_Kfet=False,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -281,8 +281,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=True,
join_kfet=False,
join_BDE=True,
join_Kfet=False,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()
@ -317,8 +317,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=True,
join_kfet=True,
join_BDE=True,
join_Kfet=True,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()
@ -353,8 +353,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=True,
join_kfet=True,
join_BDE=True,
join_Kfet=True,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()

Some files were not shown because too many files have changed in this diff Show More