1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Merge branch 'oauth2' into 'beta'

Implement OAuth2 scopes based on permissions

See merge request bde/nk20!170
This commit is contained in:
ynerant 2021-09-02 19:18:43 +00:00
commit 9930c48253
42 changed files with 1169 additions and 289 deletions

View File

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
from member.models import Club from member.models import Club
from note.models import Note, NoteUser from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete, DateTimePickerInput from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models import Activity, Guest 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"].initial = Club.objects.get(name="Kfet")
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet" self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
clubs = list(Club.objects.filter(PermissionBackend clubs = list(Club.objects.filter(PermissionBackend
.filter_queryset(get_current_authenticated_user(), Club, "view")).all()) .filter_queryset(get_current_request(), Club, "view")).all())
shuffle(clubs) shuffle(clubs)
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."

View File

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

View File

@ -9,7 +9,6 @@ from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session
from note.models import Alias from note.models import Alias
from .serializers import UserSerializer, ContentTypeSerializer from .serializers import UserSerializer, ContentTypeSerializer
@ -25,9 +24,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
user = self.request.user return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet): class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
@ -40,9 +37,7 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
user = self.request.user return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class UserViewSet(ReadProtectedModelViewSet): class UserViewSet(ReadProtectedModelViewSet):

View File

@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from note.models import NoteUser, Alias from note.models import NoteUser, Alias
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip from note_kfet.middlewares import get_current_request
from .models import Changelog from .models import Changelog
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
previous = instance._previous previous = instance._previous
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip() request = get_current_request()
if user is None: if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # 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 # 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 # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
# else: # else:
if note.exists(): if note.exists():
user = note.get().user 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 # noinspection PyProtectedMember
if user is not None and instance._meta.label_lower == "auth.user" and previous: if request is not None and instance._meta.label_lower == "auth.user" and previous:
# On n'enregistre pas les connexions # On n'enregistre pas les connexions
if instance.last_login != previous.last_login: if instance.last_login != previous.last_login:
return return
@ -121,9 +135,9 @@ def delete_object(sender, instance, **kwargs):
return return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip() request = get_current_request()
if user is None: if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # 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 # 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 # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
# else: # else:
if note.exists(): if note.exists():
user = note.get().user 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 # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
class CustomSerializer(ModelSerializer): class CustomSerializer(ModelSerializer):

View File

@ -6,7 +6,7 @@ import hashlib
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import PBKDF2PasswordHasher from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare from django.utils.crypto import constant_time_compare
from note_kfet.middlewares import get_current_authenticated_user, get_current_session from note_kfet.middlewares import get_current_request
class CustomNK15Hasher(PBKDF2PasswordHasher): class CustomNK15Hasher(PBKDF2PasswordHasher):
@ -24,16 +24,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
def must_update(self, encoded): def must_update(self, encoded):
if settings.DEBUG: if settings.DEBUG:
current_user = get_current_authenticated_user() # Small hack to let superusers to impersonate people.
# Don't change their password.
request = get_current_request()
current_user = request.user
if current_user is not None and current_user.is_superuser: if current_user is not None and current_user.is_superuser:
return False return False
return True return True
def verify(self, password, encoded): def verify(self, password, encoded):
if settings.DEBUG: if settings.DEBUG:
current_user = get_current_authenticated_user() # 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
if current_user is not None and current_user.is_superuser\ if current_user is not None and current_user.is_superuser\
and get_current_session().get("permission_mask", -1) >= 42: and request.session.get("permission_mask", -1) >= 42:
return True return True
if '|' in encoded: if '|' in encoded:
@ -51,8 +57,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
def verify(self, password, encoded): def verify(self, password, encoded):
if settings.DEBUG: if settings.DEBUG:
current_user = get_current_authenticated_user() # 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
if current_user is not None and current_user.is_superuser\ if current_user is not None and current_user.is_superuser\
and get_current_session().get("permission_mask", -1) >= 42: and request.session.get("permission_mask", -1) >= 42:
return True return True
return super().verify(password, encoded) return super().verify(password, encoded)

View File

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

View File

@ -5,32 +5,98 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="alert alert-info"> <div class="row mt-4">
<h4>À quoi sert un jeton d'authentification ?</h4> <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">
<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>.<br /> Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code> depuis un client externe.<br />
pour pouvoir vous identifier.<br /><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 />
Une documentation de l'API arrivera ultérieurement. La documentation de l'API est disponible ici :
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
</div>
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
{% else %}
<em>caché</em> (<a href="?show">montrer</a>)
{% 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>
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
{% else %}
<em>caché</em> (<a href="?show">montrer</a>)
{% endif %}
<br />
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
</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 %} {% endblock %}

View File

@ -21,7 +21,7 @@ from rest_framework.authtoken.models import Token
from note.models import Alias, NoteUser from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable from note.tables import HistoryTable, AliasTable
from note_kfet.middlewares import _set_current_user_and_ip from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -41,7 +41,8 @@ class CustomLoginView(LoginView):
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
logout(self.request) logout(self.request)
_set_current_user_and_ip(form.get_user(), self.request.session, None) self.request.user = form.get_user()
_set_current_request(self.request)
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form) return super().form_valid(form)
@ -70,7 +71,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.") form.fields['email'].help_text = _("This address must be valid.")
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile): if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
context['profile_form'] = self.profile_form(instance=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) data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency: if not self.object.profile.report_frequency:
@ -153,13 +154,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at")\ .order_by("-created_at")\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-') history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\ club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\ .filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
.order_by("club__name", "-date_start") .order_by("club__name", "-date_start")
# Display only the most recent membership # Display only the most recent membership
club_list = club_list.distinct("club__name")\ club_list = club_list.distinct("club__name")\
@ -176,21 +177,20 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note.is_active = True modified_note.is_active = True
modified_note.inactivity_reason = 'manual' modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\ context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_noteuser_is_active", .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
modified_note)
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk) old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
modified_note.inactivity_reason = 'forced' modified_note.inactivity_reason = 'forced'
modified_note._force_save = True modified_note._force_save = True
modified_note.save() modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\ context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_is_active", modified_note) .check_perm(self.request, "note.change_note_is_active", modified_note)
old_note._force_save = True old_note._force_save = True
old_note._no_signal = True old_note._no_signal = True
old_note.save() old_note.save()
modified_note.refresh_from_db() modified_note.refresh_from_db()
modified_note.is_active = True modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\ context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_is_active", modified_note) .check_perm(self.request, "note.change_note_is_active", modified_note)
return context return context
@ -237,7 +237,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\ pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
.filter(profile__registration_valid=False) .filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists() context["can_manage_registrations"] = pre_registered_users.exists()
return context return context
@ -256,8 +256,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable( context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
normalized_name="", normalized_name="",
@ -382,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club( context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
name="", name="",
email="club@example.com", email="club@example.com",
)) ))
@ -404,7 +404,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = context["club"] club = context["club"]
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates() club.update_membership_dates()
# managers list # managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club", 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-") context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history # transaction history
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
.order_by('-created_at') .order_by('-created_at')
history_table = HistoryTable(club_transactions, prefix="history-") history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) 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_member = Membership.objects.filter(
club=club, club=club,
date_end__gte=date.today() - timedelta(days=15), date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\ ).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
.order_by("user__username", "-date_start") .order_by("user__username", "-date_start")
# Display only the most recent membership # Display only the most recent membership
club_member = club_member.distinct("user__username")\ club_member = club_member.distinct("user__username")\
@ -459,8 +459,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable(note.alias.filter( context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
normalized_name="", normalized_name="",
@ -535,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form = context['form'] form = context['form']
if "club_pk" in self.kwargs: # We create a new membership. if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None) .get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid 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. # Ensure that the user is member of the parent club and all its the family tree.
@ -683,7 +683,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
# Get the club that is concerned by the membership # Get the club that is concerned by the membership
if "club_pk" in self.kwargs: # get from url of new membership if "club_pk" in self.kwargs: # get from url of new membership
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
.get(pk=self.kwargs["club_pk"]) .get(pk=self.kwargs["club_pk"])
user = form.instance.user user = form.instance.user
old_membership = None old_membership = None
@ -867,7 +867,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = Club.objects.filter( club = Club.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Club, "view") PermissionBackend.filter_queryset(self.request, Club, "view")
).get(pk=self.kwargs["pk"]) ).get(pk=self.kwargs["pk"])
context["club"] = club context["club"] = club

View File

@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer from rest_polymorphic.serializers import PolymorphicSerializer
from member.api.serializers import MembershipSerializer from member.api.serializers import MembershipSerializer
from member.models import Membership from member.models import Membership
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from rest_framework.utils import model_meta 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 # If the user has no right to see the note, then we only display the note identifier
return NotePolymorphicSerializer().to_representation(obj.note)\ return NotePolymorphicSerializer().to_representation(obj.note)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\ if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
else dict( else dict(
id=obj.note.id, id=obj.note.id,
name=str(obj.note), name=str(obj.note),
@ -142,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
def get_membership(self, obj): def get_membership(self, obj):
if isinstance(obj.note, NoteUser): if isinstance(obj.note, NoteUser):
memberships = Membership.objects.filter( memberships = Membership.objects.filter(
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter( PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
user=obj.note.user, user=obj.note.user,
club=2, # Kfet club=2, # Kfet
).order_by("-date_start") ).order_by("-date_start")

View File

@ -10,7 +10,6 @@ from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from note_kfet.middlewares import get_current_session
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
@ -40,12 +39,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
Parse query and apply filters. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
user = self.request.user queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
get_current_session().setdefault("permission_mask", 42) | PermissionBackend.filter_queryset(self.request, NoteUser, "view")
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view") | PermissionBackend.filter_queryset(self.request, NoteClub, "view")
| PermissionBackend.filter_queryset(user, NoteUser, "view") | PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
| PermissionBackend.filter_queryset(user, NoteClub, "view") .distinct()
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
@ -67,7 +65,8 @@ class AliasViewSet(ReadProtectedModelViewSet):
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ] filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ] ordering_fields = ['name', 'normalized_name', ]
def get_serializer_class(self): def get_serializer_class(self):
@ -118,7 +117,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend] filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ] filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ] ordering_fields = ['name', 'normalized_name', ]
def get_queryset(self): def get_queryset(self):
@ -205,7 +205,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
ordering_fields = ['created_at', 'amount', ] ordering_fields = ['created_at', 'amount', ]
def get_queryset(self): def get_queryset(self):
user = self.request.user return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
.order_by("created_at", "id") .order_by("created_at", "id")

View File

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

View File

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

View File

@ -4,12 +4,12 @@
from datetime import date from datetime import date
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, AnonymousUser from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, F from django.db.models import Q, F
from django.utils import timezone from django.utils import timezone
from note.models import Note, NoteUser, NoteClub, NoteSpecial from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet.middlewares import get_current_session from note_kfet.middlewares import get_current_request
from member.models import Membership, Club from member.models import Membership, Club
from .decorators import memoize from .decorators import memoize
@ -26,14 +26,31 @@ class PermissionBackend(ModelBackend):
@staticmethod @staticmethod
@memoize @memoize
def get_raw_permissions(user, t): def get_raw_permissions(request, t):
""" """
Query permissions of a certain type for a user, then memoize it. Query permissions of a certain type for a user, then memoize it.
:param user: The owner of the permissions :param request: The current request
:param t: The type of the permissions: view, change, add or delete :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 :return: The queryset of the permissions of the user (memoized) grouped by clubs
""" """
if isinstance(user, AnonymousUser): 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:
# Unauthenticated users have no permissions # Unauthenticated users have no permissions
return Permission.objects.none() return Permission.objects.none()
@ -43,7 +60,7 @@ class PermissionBackend(ModelBackend):
for membership in memberships: for membership in memberships:
for role in membership.roles.all(): for role in membership.roles.all():
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all(): for perm in role.permissions.filter(permission_filter(membership), type=t).all():
if not perm.permanent: if not perm.permanent:
if membership.date_start > date.today() or membership.date_end < date.today(): if membership.date_start > date.today() or membership.date_end < date.today():
continue continue
@ -52,16 +69,22 @@ class PermissionBackend(ModelBackend):
return perms return perms
@staticmethod @staticmethod
def permissions(user, model, type): def permissions(request, model, type):
""" """
List all permissions of the given user that applies to a given model and a give type List all permissions of the given user that applies to a given model and a give type
:param user: The owner of the permissions :param request: The current request
:param model: The model that the permissions shoud apply :param model: The model that the permissions shoud apply
:param type: The type of the permissions: view, change, add or delete :param type: The type of the permissions: view, change, add or delete
:return: A generator of the requested permissions :return: A generator of the requested permissions
""" """
for permission in PermissionBackend.get_raw_permissions(user, type): 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):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership: if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
continue continue
@ -88,20 +111,26 @@ class PermissionBackend(ModelBackend):
@staticmethod @staticmethod
@memoize @memoize
def filter_queryset(user, model, t, field=None): def filter_queryset(request, model, t, field=None):
""" """
Filter a queryset by considering the permissions of a given user. Filter a queryset by considering the permissions of a given user.
:param user: The owner of the permissions that are fetched :param request: The current request
:param model: The concerned model of the queryset :param model: The concerned model of the queryset
:param t: The type of modification (view, add, change, delete) :param t: The type of modification (view, add, change, delete)
:param field: The field of the model to test, if concerned :param field: The field of the model to test, if concerned
:return: A query that corresponds to the filter to give to a queryset :return: A query that corresponds to the filter to give to a queryset
""" """
if user is None or isinstance(user, AnonymousUser): 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:
# Anonymous users can't do anything # Anonymous users can't do anything
return Q(pk=-1) return Q(pk=-1)
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42: if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
# Superusers have all rights # Superusers have all rights
return Q() return Q()
@ -110,7 +139,7 @@ class PermissionBackend(ModelBackend):
# Never satisfied # Never satisfied
query = Q(pk=-1) query = Q(pk=-1)
perms = PermissionBackend.permissions(user, model, t) perms = PermissionBackend.permissions(request, model, t)
for perm in perms: for perm in perms:
if perm.field and field != perm.field: if perm.field and field != perm.field:
continue continue
@ -122,7 +151,7 @@ class PermissionBackend(ModelBackend):
@staticmethod @staticmethod
@memoize @memoize
def check_perm(user_obj, perm, obj=None): def check_perm(request, perm, obj=None):
""" """
Check is the given user has the permission over a given object. Check is the given user has the permission over a given object.
The result is then memoized. The result is then memoized.
@ -130,10 +159,15 @@ class PermissionBackend(ModelBackend):
primary key, the result is not memoized. Moreover, the right could change primary key, the result is not memoized. Moreover, the right could change
(e.g. for a transaction, the balance of the user could change) (e.g. for a transaction, the balance of the user could change)
""" """
if user_obj is None or isinstance(user_obj, AnonymousUser): user_obj = request.user
return False sess = request.session
sess = get_current_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:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42: if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True return True
@ -147,16 +181,19 @@ class PermissionBackend(ModelBackend):
ct = ContentType.objects.get_for_model(obj) ct = ContentType.objects.get_for_model(obj)
if any(permission.applies(obj, perm_type, perm_field) if any(permission.applies(obj, perm_type, perm_field)
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)): for permission in PermissionBackend.permissions(request, ct, perm_type)):
return True return True
return False return False
def has_perm(self, user_obj, perm, obj=None): def has_perm(self, user_obj, perm, obj=None):
return PermissionBackend.check_perm(user_obj, perm, obj) # 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)
def has_module_perms(self, user_obj, app_label): def has_module_perms(self, user_obj, app_label):
return False return False
def get_all_permissions(self, user_obj, obj=None): def get_all_permissions(self, user_obj, obj=None):
ct = ContentType.objects.get_for_model(obj) ct = ContentType.objects.get_for_model(obj)
return list(self.permissions(user_obj, ct, "view")) return list(self.permissions(get_current_request(), ct, "view"))

View File

@ -5,7 +5,7 @@ from functools import lru_cache
from time import time from time import time
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from note_kfet.middlewares import get_current_session from note_kfet.middlewares import get_current_request
def memoize(f): def memoize(f):
@ -48,11 +48,11 @@ def memoize(f):
last_collect = time() last_collect = time()
# If there is no session, then we don't memoize anything. # If there is no session, then we don't memoize anything.
sess = get_current_session() request = get_current_request()
if sess is None or sess.session_key is None: if request is None or request.session is None or request.session.session_key is None:
return f(*args, **kwargs) return f(*args, **kwargs)
sess_key = sess.session_key sess_key = request.session.session_key
if sess_key not in sess_funs: if sess_key not in sess_funs:
# lru_cache makes the job of memoization # lru_cache makes the job of memoization
# We store only the 512 latest data per session. It has to be enough. # We store only the 512 latest data per session. It has to be enough.

View File

@ -45,7 +45,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
perms = self.get_required_object_permissions(request.method, model_cls) perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj): # if not user.has_perms(perms, obj):
if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms): if not all(PermissionBackend.check_perm(request, perm, obj) for perm in perms):
# If the user does not have permissions we need to determine if # If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see # they have read permissions to see 403, or not, and simply see
# a 404 response. # a 404 response.

34
apps/permission/scopes.py Normal file
View File

@ -0,0 +1,34 @@
# 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

@ -3,7 +3,7 @@
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -16,6 +16,9 @@ EXCLUDED = [
'contenttypes.contenttype', 'contenttypes.contenttype',
'logs.changelog', 'logs.changelog',
'migrations.migration', 'migrations.migration',
'oauth2_provider.accesstoken',
'oauth2_provider.grant',
'oauth2_provider.refreshtoken',
'sessions.session', 'sessions.session',
] ]
@ -31,8 +34,8 @@ def pre_save_object(sender, instance, **kwargs):
if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"): if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"):
return return
user = get_current_authenticated_user() request = get_current_request()
if user is None: if request is None:
# Action performed on shell is always granted # Action performed on shell is always granted
return return
@ -45,7 +48,7 @@ def pre_save_object(sender, instance, **kwargs):
# We check if the user can change the model # We check if the user can change the model
# If the user has all right on a model, then OK # If the user has all right on a model, then OK
if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance): if PermissionBackend.check_perm(request, app_label + ".change_" + model_name, instance):
return return
# In the other case, we check if he/she has the right to change one field # In the other case, we check if he/she has the right to change one field
@ -58,7 +61,8 @@ def pre_save_object(sender, instance, **kwargs):
# If the field wasn't modified, no need to check the permissions # If the field wasn't modified, no need to check the permissions
if old_value == new_value: if old_value == new_value:
continue continue
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
instance):
raise PermissionDenied( raise PermissionDenied(
_("You don't have the permission to change the field {field} on this instance of model" _("You don't have the permission to change the field {field} on this instance of model"
" {app_label}.{model_name}.") " {app_label}.{model_name}.")
@ -66,7 +70,7 @@ def pre_save_object(sender, instance, **kwargs):
) )
else: else:
# We check if the user has right to add the object # We check if the user has right to add the object
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) has_perm = PermissionBackend.check_perm(request, app_label + ".add_" + model_name, instance)
if not has_perm: if not has_perm:
raise PermissionDenied( raise PermissionDenied(
@ -87,8 +91,8 @@ def pre_delete_object(instance, **kwargs):
# Don't check permissions on force-deleted objects # Don't check permissions on force-deleted objects
return return
user = get_current_authenticated_user() request = get_current_request()
if user is None: if request is None:
# Action performed on shell is always granted # Action performed on shell is always granted
return return
@ -97,7 +101,7 @@ def pre_delete_object(instance, **kwargs):
model_name = model_name_full[1] model_name = model_name_full[1]
# We check if the user has rights to delete the object # We check if the user has rights to delete the object
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): if not PermissionBackend.check_perm(request, app_label + ".delete_" + model_name, instance):
raise PermissionDenied( raise PermissionDenied(
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.") _("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)) .format(app_label=app_label, model_name=model_name))

View File

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

View File

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

View File

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

@ -66,9 +66,11 @@ class UserCreateView(CreateView):
profile_form.instance.user = user profile_form.instance.user = user
profile = profile_form.save(commit=False) profile = profile_form.save(commit=False)
user.profile = profile user.profile = profile
user._force_save = True
user.save() user.save()
user.refresh_from_db() user.refresh_from_db()
profile.user = user profile.user = user
profile._force_save = True
profile.save() profile.save()
user.profile.send_email_validation_link() user.profile.send_email_validation_link()
@ -110,7 +112,9 @@ class UserValidateView(TemplateView):
self.validlink = True self.validlink = True
user.is_active = user.profile.registration_valid or user.is_superuser user.is_active = user.profile.registration_valid or user.is_superuser
user.profile.email_confirmed = True user.profile.email_confirmed = True
user._force_save = True
user.save() user.save()
user.profile._force_save = True
user.profile.save() user.profile.save()
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400) return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
@ -384,7 +388,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
Delete the pre-registered user which id is given in the URL. Delete the pre-registered user which id is given in the URL.
""" """
user = User.objects.filter(profile__registration_valid=False)\ user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\ .filter(PermissionBackend.filter_queryset(request, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"]) .get(pk=self.kwargs["pk"])
# Delete associated soge credits before # Delete associated soge credits before
SogeCredit.objects.filter(user=user).delete() SogeCredit.objects.filter(user=user).delete()

View File

@ -107,7 +107,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
name="", name="",
address="", address="",
) )
if not PermissionBackend.check_perm(self.request.user, "treasury.add_invoice", sample_invoice): if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
raise PermissionDenied(_("You are not able to see the treasury interface.")) raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -194,7 +194,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
def get(self, request, **kwargs): def get(self, request, **kwargs):
pk = kwargs["pk"] pk = kwargs["pk"]
invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk) invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request, Invoice, "view")).get(pk=pk)
tex = invoice.tex tex = invoice.tex
try: try:
@ -259,7 +259,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
context["table"] = RemittanceTable( context["table"] = RemittanceTable(
data=Remittance.objects.filter( data=Remittance.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) PermissionBackend.filter_queryset(self.request, Remittance, "view")).all())
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
return context return context
@ -281,7 +281,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
remittance_type_id=1, remittance_type_id=1,
comment="", comment="",
) )
if not PermissionBackend.check_perm(self.request.user, "treasury.add_remittance", sample_remittance): if not PermissionBackend.check_perm(self.request, "treasury.add_remittance", sample_remittance):
raise PermissionDenied(_("You are not able to see the treasury interface.")) raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -290,7 +290,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
opened_remittances = RemittanceTable( opened_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=False).filter( data=Remittance.objects.filter(closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
prefix="opened-remittances-", prefix="opened-remittances-",
) )
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10) opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
@ -298,7 +298,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
closed_remittances = RemittanceTable( closed_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter( data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
prefix="closed-remittances-", prefix="closed-remittances-",
) )
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10) closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
@ -307,7 +307,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
no_remittance_tr = SpecialTransactionTable( no_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).filter( specialtransactionproxy__remittance=None).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
exclude=('remittance_remove', ), exclude=('remittance_remove', ),
prefix="no-remittance-", prefix="no-remittance-",
) )
@ -317,7 +317,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
with_remittance_tr = SpecialTransactionTable( with_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance__closed=False).filter( specialtransactionproxy__remittance__closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
exclude=('remittance_add', ), exclude=('remittance_add', ),
prefix="with-remittance-", prefix="with-remittance-",
) )
@ -342,7 +342,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter( data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all() PermissionBackend.filter_queryset(self.request, Remittance, "view")).all()
context["special_transactions"] = SpecialTransactionTable( context["special_transactions"] = SpecialTransactionTable(
data=data, data=data,
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))

View File

@ -8,7 +8,7 @@ from django.urls import reverse_lazy
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_tables2 import A from django_tables2 import A
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
@ -85,7 +85,7 @@ class WEIRegistrationTable(tables.Table):
def render_validate(self, record): def render_validate(self, record):
hasperm = PermissionBackend.check_perm( hasperm = PermissionBackend.check_perm(
get_current_authenticated_user(), "wei.add_weimembership", WEIMembership( get_current_request(), "wei.add_weimembership", WEIMembership(
club=record.wei, club=record.wei,
user=record.user, user=record.user,
date_start=date.today(), date_start=date.today(),
@ -110,7 +110,7 @@ class WEIRegistrationTable(tables.Table):
f"title=\"{tooltip}\" href=\"{url}\">{text}</a>") f"title=\"{tooltip}\" href=\"{url}\">{text}</a>")
def render_delete(self, record): def render_delete(self, record):
hasperm = PermissionBackend.check_perm(get_current_authenticated_user(), "wei.delete_weimembership", record) hasperm = PermissionBackend.check_perm(get_current_request(), "wei.delete_weimembership", record)
return _("Delete") if hasperm else format_html("<span class='no-perm'></span>") return _("Delete") if hasperm else format_html("<span class='no-perm'></span>")
class Meta: class Meta:

View File

@ -57,7 +57,7 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub( context["can_create_wei"] = PermissionBackend.check_perm(self.request, "wei.add_weiclub", WEIClub(
name="", name="",
email="weiclub@example.com", email="weiclub@example.com",
year=0, year=0,
@ -112,7 +112,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club = context["club"] club = context["club"]
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \ .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \
.order_by('-created_at', '-id') .order_by('-created_at', '-id')
history_table = HistoryTable(club_transactions, prefix="history-") history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
@ -121,13 +121,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club_member = WEIMembership.objects.filter( club_member = WEIMembership.objects.filter(
club=club, club=club,
date_end__gte=date.today(), date_end__gte=date.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view")) ).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
membership_table = WEIMembershipTable(data=club_member, prefix="membership-") membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
context['member_list'] = membership_table context['member_list'] = membership_table
pre_registrations = WEIRegistration.objects.filter( pre_registrations = WEIRegistration.objects.filter(
PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter( PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter(
membership=None, membership=None,
wei=club wei=club
) )
@ -142,7 +142,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
my_registration = None my_registration = None
context["my_registration"] = my_registration context["my_registration"] = my_registration
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \ buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name") .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
bus_table = BusTable(data=buses, prefix="bus-") bus_table = BusTable(data=buses, prefix="bus-")
context['buses'] = bus_table context['buses'] = bus_table
@ -167,7 +167,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
emergency_contact_phone="No", emergency_contact_phone="No",
) )
context["can_add_first_year_member"] = PermissionBackend \ context["can_add_first_year_member"] = PermissionBackend \
.check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration) .check_perm(self.request, "wei.add_weiregistration", empty_fy_registration)
# Check if the user has the right to create a registration of a random old member. # Check if the user has the right to create a registration of a random old member.
empty_old_registration = WEIRegistration( empty_old_registration = WEIRegistration(
@ -180,13 +180,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
emergency_contact_phone="No", emergency_contact_phone="No",
) )
context["can_add_any_member"] = PermissionBackend \ context["can_add_any_member"] = PermissionBackend \
.check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration) .check_perm(self.request, "wei.add_weiregistration", empty_old_registration)
empty_bus = Bus( empty_bus = Bus(
wei=club, wei=club,
name="", name="",
) )
context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus) context["can_add_bus"] = PermissionBackend.check_perm(self.request, "wei.add_bus", empty_bus)
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists() context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
@ -370,13 +370,13 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["club"] = self.object.wei context["club"] = self.object.wei
bus = self.object bus = self.object
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \ teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request, BusTeam, "view")) \
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name") .filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
teams_table = BusTeamTable(data=teams, prefix="team-") teams_table = BusTeamTable(data=teams, prefix="team-")
context["teams"] = teams_table context["teams"] = teams_table
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
self.request.user, WEIMembership, "view")).filter(bus=bus) self.request, WEIMembership, "view")).filter(bus=bus)
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
context["memberships"] = memberships_table context["memberships"] = memberships_table
@ -469,7 +469,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["club"] = self.object.bus.wei context["club"] = self.object.bus.wei
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
self.request.user, WEIMembership, "view")).filter(team=self.object) self.request, WEIMembership, "view")).filter(team=self.object)
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
context["memberships"] = memberships_table context["memberships"] = memberships_table
@ -659,7 +659,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
data=self.request.POST if self.request.POST else None) data=self.request.POST if self.request.POST else None)
for field_name, field in membership_form.fields.items(): for field_name, field in membership_form.fields.items():
if not PermissionBackend.check_perm( if not PermissionBackend.check_perm(
self.request.user, "wei.change_membership_" + field_name, self.object.membership): self.request, "wei.change_membership_" + field_name, self.object.membership):
field.widget = HiddenInput() field.widget = HiddenInput()
del membership_form.fields["credit_type"] del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"] del membership_form.fields["credit_amount"]
@ -668,7 +668,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
del membership_form.fields["bank"] del membership_form.fields["bank"]
context["membership_form"] = membership_form context["membership_form"] = membership_form
elif not self.object.first_year and PermissionBackend.check_perm( elif not self.object.first_year and PermissionBackend.check_perm(
self.request.user, "wei.change_weiregistration_information_json", self.object): self.request, "wei.change_weiregistration_information_json", self.object):
choose_bus_form = WEIChooseBusForm( choose_bus_form = WEIChooseBusForm(
self.request.POST if self.request.POST else dict( self.request.POST if self.request.POST else dict(
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(), bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
@ -704,7 +704,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
membership_form.save() membership_form.save()
# If it is not validated and if this is an old member, then we update the choices # If it is not validated and if this is an old member, then we update the choices
elif not form.instance.first_year and PermissionBackend.check_perm( elif not form.instance.first_year and PermissionBackend.check_perm(
self.request.user, "wei.change_weiregistration_information_json", self.object): self.request, "wei.change_weiregistration_information_json", self.object):
choose_bus_form = WEIChooseBusForm(self.request.POST) choose_bus_form = WEIChooseBusForm(self.request.POST)
if not choose_bus_form.is_valid(): if not choose_bus_form.is_valid():
return self.form_invalid(form) return self.form_invalid(form)
@ -726,7 +726,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object) survey = CurrentSurvey(self.object)
if not survey.is_complete(): if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
if PermissionBackend.check_perm(self.request.user, "wei.add_weimembership", WEIMembership( if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
club=self.object.wei, club=self.object.wei,
user=self.object.user, user=self.object.user,
date_start=date.today(), date_start=date.today(),
@ -753,7 +753,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
if today > wei.membership_end: if today > wei.membership_end:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object): if not PermissionBackend.check_perm(self.request, "wei.delete_weiregistration", object):
raise PermissionDenied(_("You don't have the right to delete this WEI registration.")) raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -1049,7 +1049,7 @@ class MemberListRenderView(LoginRequiredMixin, View):
""" """
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view")) qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by( qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
Lower('bus__name'), Lower('bus__name'),
Lower('team__name'), Lower('team__name'),

View File

@ -5,19 +5,10 @@ L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supporté
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi
de transmettre des informations à un service tiers tels que des informations personnelles, de transmettre des informations à un service tiers tels que des informations personnelles,
le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur
quelles données sont effectivement collectées. quelles données sont effectivement collectées. Ainsi, il est possible de développer des
appplications tierces qui peuvent se baser sur les données de la Note Kfet ou encore
faire des transactions.
.. danger::
L'implémentation actuelle ne permet pas de choisir quels droits on offre. Se connecter
par OAuth2 offre actuellement exactement les mêmes permissions que l'on n'aurait
normalement, avec le masque le plus haut, y compris en écriture.
Faites alors très attention lorsque vous vous connectez à un service tiers via OAuth2,
et contrôlez bien exactement ce que l'application fait de vos données, à savoir si
elle ignore bien tout ce dont elle n'a pas besoin.
À l'avenir, la fenêtre d'authentification pourra vous indiquer clairement quels
paramètres sont collectés.
Configuration du serveur Configuration du serveur
------------------------ ------------------------
@ -44,7 +35,15 @@ l'authentification OAuth2. On adapte alors la configuration pour permettre cela
... ...
} }
On ajoute les routes dans ``urls.py`` : On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions fines :
.. code:: python
OAUTH2_PROVIDER = {
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
}
On ajoute enfin les routes dans ``urls.py`` :
.. code:: python .. code:: python
@ -58,8 +57,7 @@ L'OAuth2 est désormais prêt à être utilisé.
Configuration client Configuration client
-------------------- --------------------
Contrairement au `CAS <cas>`_, n'importe qui peut en théorie créer une application OAuth2. Contrairement au `CAS <cas>`_, n'importe qui peut créer une application OAuth2.
En théorie, car pour l'instant les permissions ne leur permettent pas.
Pour créer une application, il faut se rendre à la page Pour créer une application, il faut se rendre à la page
`/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``, `/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``,
@ -72,14 +70,30 @@ Il vous suffit de donner à votre application :
* L'identifiant client (client-ID) * L'identifiant client (client-ID)
* La clé secrète * La clé secrète
* Les scopes : sous-ensemble de ``[read, write]`` (ignoré pour l'instant, cf premier paragraphe) * Les scopes, qui peuvent être récupérées sur cette page : `<https://note.crans.org/permission/scopes/>`_
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_ * L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_ * L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
* L'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_ * Si besoin, l'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
du format renvoyé. du format renvoyé.
.. warning::
Un petit mot sur les scopes : tel qu'implémenté, une scope est une permission unitaire
(telle que décrite dans le modèle ``Permission``) associée à un club. Ainsi, un jeton
a accès à une scope si et seulement si le/la propriétaire du jeton dispose d'une adhésion
courante dans le club lié à la scope qui lui octroie cette permission.
Par exemple, un jeton pourra avoir accès à la permission de créer des transactions en lien
avec un club si et seulement si le propriétaire du jeton est trésorier du club.
La vérification des droits du propriétaire est faite systématiquement, afin de ne pas
faire confiance au jeton en cas de droits révoqués à son propriétaire.
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
jetons.
Avec Django-allauth Avec Django-allauth
################### ###################
@ -131,3 +145,97 @@ alors autant le faire via un shell python :
Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous
connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont
récupérés. Les autres données sont stockées mais inutilisées. récupérés. Les autres données sont stockées mais inutilisées.
Application personnalisée
#########################
Ce modèle vous permet de créer vos propres applications à interfacer avec la Note Kfet.
Commencez par créer une application : `<https://note.crans.org/o/applications/register>`_.
Dans ``Client type``, choisissez ``Confidential`` si des informations confidentielles sont
amenées à transiter, sinon ``public``. Choisissez ``Authorization code`` dans
``Authorization grant type``.
Dans ``Redirect uris``, vous devez insérer l'ensemble des URL autorisées à être redirigées
à la suite d'une autorisation OAuth2. La première URL entrée sera l'URL par défaut dans le
cas où elle n'est pas explicitement indiquée lors de l'autorisation.
.. note::
À des fins de tests, il est possible de laisser `<http://localhost/>`_ pour faire des
appels à la main en récupérant le jeton d'autorisation.
Lorsqu'un client veut s'authentifier via la Note Kfet, il va devoir accéder à une page
d'authentification. La page d'autorisation est `<https://note.crans.org/o/authorize/>`_,
c'est sur cette page qu'il faut rediriger les utilisateurs. Il faut mettre en paramètre GET :
* ``client_id`` : l'identifiant client de l'application (public) ;
* ``response_type`` : mettre ``code`` ;
* ``scope`` : l'ensemble des scopes demandés, séparés par des espaces. Ces scopes peuvent
être récupérés sur la page `<https://note.crans.org/permission/scopes/>`_.
* ``redirect_uri`` : l'URL sur laquelle rediriger qui récupérera le code d'accès. Doit être
autorisée par l'application. À des fins de test, peut être `<http://localhost/>`_.
* ``state`` : optionnel, peut être utilisé pour permettre au client de détecter des requêtes
provenant d'autres sites.
Sur cette page, les permissions demandées seront listées, et l'utilisateur aura le choix
d'accepter ou non. Dans les deux cas, l'utilisateur sera redirigée vers ``redirect_uri``,
avec pour paramètre GET soit le message d'erreur, soit un paramètre ``code`` correspondant
au code d'autorisation.
Une fois ce code d'autorisation récupéré, il faut désormais récupérer le jeton d'accès.
Il faut pour cela aller sur l'URL `<https://note.crans.org/o/token/>`_, effectuer une
requête POST avec pour arguments :
* ``client_id`` ;
* ``client_secret`` ;
* ``grant_type`` : mettre ``authorization_code`` ;
* ``code`` : le code généré.
À noter que le code fourni n'est disponible que pendant quelques secondes.
À des fins de tests, on peut envoyer la requête avec ``curl`` :
.. code:: bash
curl -X POST https://note.crans.org/o/token/ -d "client_id=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&grant_type=authorization_code&code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Le serveur renverra si tout se passe bien une réponse JSON :
.. code:: json
{
"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "1_1 1_2",
"refresh_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
On note donc 2 jetons différents : un d'accès et un de rafraîchissement. Le jeton d'accès
est celui qui sera donné à l'API pour s'authentifier, et qui expire au bout de quelques
heures.
Il suffit désormais d'ajouter l'en-tête ``Authorization: Bearer ACCESS_TOKEN`` pour se
connecter à la note grâce à ce jeton d'accès.
Pour tester :
.. code:: bash
curl https://note.crans.org/api/me -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
En cas d'expiration de ce jeton d'accès, il est possible de le renouveler grâce au jeton
de rafraichissement à usage unique. Il suffit pour cela de refaire une requête sur la page
`<https://note.crans.org/o/token/>`_ avec pour paramètres :
* ``client_id`` ;
* ``client_secret`` ;
* ``grant_type`` : mettre ``refresh_token`` ;
* ``refresh_token`` : le jeton de rafraîchissement.
Le serveur vous fournira alors une nouvelle paire de jetons, comme précédemment.
À noter qu'un jeton de rafraîchissement est à usage unique.
N'hésitez pas à vous renseigner sur OAuth2 pour plus d'informations.

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-08-29 14:06+0200\n" "POT-Creation-Date: 2021-06-15 21:17+0200\n"
"PO-Revision-Date: 2020-11-16 20:02+0000\n" "PO-Revision-Date: 2020-11-16 20:02+0000\n"
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n" "Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -540,8 +540,8 @@ msgstr "Taille maximale : 2 Mo"
msgid "This image cannot be loaded." msgid "This image cannot be loaded."
msgstr "Cette image ne peut pas être chargée." msgstr "Cette image ne peut pas être chargée."
#: apps/member/forms.py:141 apps/member/views.py:101 #: apps/member/forms.py:141 apps/member/views.py:100
#: apps/registration/forms.py:33 apps/registration/views.py:258 #: apps/registration/forms.py:33 apps/registration/views.py:254
msgid "An alias with a similar name already exists." msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà." msgstr "Un alias avec un nom similaire existe déjà."
@ -900,7 +900,7 @@ msgid "Account #"
msgstr "Compte n°" msgstr "Compte n°"
#: apps/member/templates/member/base.html:48 #: apps/member/templates/member/base.html:48
#: apps/member/templates/member/base.html:62 apps/member/views.py:58 #: apps/member/templates/member/base.html:62 apps/member/views.py:59
#: apps/registration/templates/registration/future_profile_detail.html:48 #: apps/registration/templates/registration/future_profile_detail.html:48
#: apps/wei/templates/wei/weimembership_form.html:117 #: apps/wei/templates/wei/weimembership_form.html:117
msgid "Update Profile" msgid "Update Profile"
@ -932,7 +932,7 @@ msgid ""
"Are you sure you want to lock this note? This will prevent any transaction " "Are you sure you want to lock this note? This will prevent any transaction "
"that would be performed, until the note is unlocked." "that would be performed, until the note is unlocked."
msgstr "" msgstr ""
"Êtes-vous sûr de vouloir verrouiller cette note ? Cela empêchera toute " "Êtes-vous sûr⋅e de vouloir verrouiller cette note ? Cela empêchera toute "
"transaction qui devrait être faite, jusqu'à ce qu'elle soit déverrouillée." "transaction qui devrait être faite, jusqu'à ce qu'elle soit déverrouillée."
#: apps/member/templates/member/base.html:104 #: apps/member/templates/member/base.html:104
@ -956,8 +956,8 @@ msgstr "Verrouiller de force"
msgid "" msgid ""
"Are you sure you want to unlock this note? Transactions will be re-enabled." "Are you sure you want to unlock this note? Transactions will be re-enabled."
msgstr "" msgstr ""
"Êtes-vous sûr de vouloir déverrouiller cette note ? Les transactions seront " "Êtes-vous sûr⋅e de vouloir déverrouiller cette note ? Les transactions "
"à nouveau possible." "seront à nouveau possible."
#: apps/member/templates/member/club_alias.html:10 #: apps/member/templates/member/club_alias.html:10
#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253 #: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
@ -1053,18 +1053,50 @@ msgstr "Changer le mot de passe"
msgid "API token" msgid "API token"
msgstr "Accès API" msgstr "Accès API"
#: apps/member/templates/member/manage_auth_tokens.html:19 #: apps/member/templates/member/manage_auth_tokens.html:12
msgid "Token authentication"
msgstr "Authentification par jeton"
#: apps/member/templates/member/manage_auth_tokens.html:28
msgid "Token" msgid "Token"
msgstr "Jeton" msgstr "Jeton"
#: apps/member/templates/member/manage_auth_tokens.html:26 #: apps/member/templates/member/manage_auth_tokens.html:35
msgid "Created" msgid "Created"
msgstr "Créé le" msgstr "Créé le"
#: apps/member/templates/member/manage_auth_tokens.html:34 #: apps/member/templates/member/manage_auth_tokens.html:39
msgid "Warning"
msgstr "Attention"
#: apps/member/templates/member/manage_auth_tokens.html:44
msgid "Regenerate token" msgid "Regenerate token"
msgstr "Regénérer le jeton" msgstr "Regénérer le jeton"
#: apps/member/templates/member/manage_auth_tokens.html:53
msgid "OAuth2 authentication"
msgstr "Authentification OAuth2"
#: apps/member/templates/member/manage_auth_tokens.html:79
msgid "Authorization:"
msgstr "Autorisation :"
#: apps/member/templates/member/manage_auth_tokens.html:83
msgid "Token:"
msgstr "Jeton :"
#: apps/member/templates/member/manage_auth_tokens.html:87
msgid "Revoke Token:"
msgstr "Révoquer le jeton :"
#: apps/member/templates/member/manage_auth_tokens.html:91
msgid "Introspect Token:"
msgstr "Introspection :"
#: apps/member/templates/member/manage_auth_tokens.html:97
msgid "Show my applications"
msgstr "Voir mes applications"
#: apps/member/templates/member/picture_update.html:35 #: apps/member/templates/member/picture_update.html:35
msgid "Nevermind" msgid "Nevermind"
msgstr "Annuler" msgstr "Annuler"
@ -1097,7 +1129,7 @@ msgstr "Sauvegarder les changements"
msgid "Registrations" msgid "Registrations"
msgstr "Inscriptions" msgstr "Inscriptions"
#: apps/member/views.py:71 apps/registration/forms.py:23 #: apps/member/views.py:72 apps/registration/forms.py:23
msgid "This address must be valid." msgid "This address must be valid."
msgstr "Cette adresse doit être valide." msgstr "Cette adresse doit être valide."
@ -1481,6 +1513,9 @@ msgstr "Pas de motif spécifié"
#: apps/treasury/templates/treasury/sogecredit_detail.html:65 #: apps/treasury/templates/treasury/sogecredit_detail.html:65
#: apps/wei/tables.py:74 apps/wei/tables.py:114 #: apps/wei/tables.py:74 apps/wei/tables.py:114
#: apps/wei/templates/wei/weiregistration_confirm_delete.html:31 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:31
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:18
#: note_kfet/templates/oauth2_provider/application_detail.html:39
#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:12
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
@ -1490,6 +1525,7 @@ msgstr "Supprimer"
#: apps/wei/templates/wei/bus_detail.html:20 #: apps/wei/templates/wei/bus_detail.html:20
#: apps/wei/templates/wei/busteam_detail.html:20 #: apps/wei/templates/wei/busteam_detail.html:20
#: apps/wei/templates/wei/busteam_detail.html:40 #: apps/wei/templates/wei/busteam_detail.html:40
#: note_kfet/templates/oauth2_provider/application_detail.html:38
msgid "Edit" msgid "Edit"
msgstr "Éditer" msgstr "Éditer"
@ -1731,7 +1767,7 @@ msgstr "s'applique au club"
msgid "role permissions" msgid "role permissions"
msgstr "permissions par rôles" msgstr "permissions par rôles"
#: apps/permission/signals.py:63 #: apps/permission/signals.py:67
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to change the field {field} on this instance " "You don't have the permission to change the field {field} on this instance "
@ -1740,7 +1776,7 @@ msgstr ""
"Vous n'avez pas la permission de modifier le champ {field} sur l'instance du " "Vous n'avez pas la permission de modifier le champ {field} sur l'instance du "
"modèle {app_label}.{model_name}." "modèle {app_label}.{model_name}."
#: apps/permission/signals.py:73 apps/permission/views.py:105 #: apps/permission/signals.py:77 apps/permission/views.py:105
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to add an instance of model {app_label}." "You don't have the permission to add an instance of model {app_label}."
@ -1749,7 +1785,7 @@ msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}." "Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
"{model_name}." "{model_name}."
#: apps/permission/signals.py:102 #: apps/permission/signals.py:106
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to delete this instance of model {app_label}." "You don't have the permission to delete this instance of model {app_label}."
@ -1799,6 +1835,25 @@ msgstr "Requête :"
msgid "No associated permission" msgid "No associated permission"
msgstr "Pas de permission associée" msgstr "Pas de permission associée"
#: apps/permission/templates/permission/scopes.html:8
msgid "Available scopes"
msgstr "Scopes disponibles"
#: apps/permission/templates/permission/scopes.html:42
#: note_kfet/templates/oauth2_provider/application_list.html:24
msgid "No applications defined"
msgstr "Pas d'application définie"
#: apps/permission/templates/permission/scopes.html:43
#: note_kfet/templates/oauth2_provider/application_list.html:25
msgid "Click here"
msgstr "Cliquez ici"
#: apps/permission/templates/permission/scopes.html:43
#: note_kfet/templates/oauth2_provider/application_list.html:25
msgid "if you want to register a new one"
msgstr "si vous voulez en enregistrer une nouvelle"
#: apps/permission/views.py:72 #: apps/permission/views.py:72
#, python-brace-format #, python-brace-format
msgid "" msgid ""
@ -1977,35 +2032,35 @@ msgstr "L'équipe de la Note Kfet."
msgid "Register new user" msgid "Register new user"
msgstr "Enregistrer un nouvel utilisateur" msgstr "Enregistrer un nouvel utilisateur"
#: apps/registration/views.py:93 #: apps/registration/views.py:95
msgid "Email validation" msgid "Email validation"
msgstr "Validation de l'adresse mail" msgstr "Validation de l'adresse mail"
#: apps/registration/views.py:95 #: apps/registration/views.py:97
msgid "Validate email" msgid "Validate email"
msgstr "Valider l'adresse e-mail" msgstr "Valider l'adresse e-mail"
#: apps/registration/views.py:137 #: apps/registration/views.py:141
msgid "Email validation unsuccessful" msgid "Email validation unsuccessful"
msgstr "La validation de l'adresse mail a échoué" msgstr "La validation de l'adresse mail a échoué"
#: apps/registration/views.py:148 #: apps/registration/views.py:152
msgid "Email validation email sent" msgid "Email validation email sent"
msgstr "L'email de vérification de l'adresse email a bien été envoyé" msgstr "L'email de vérification de l'adresse email a bien été envoyé"
#: apps/registration/views.py:156 #: apps/registration/views.py:160
msgid "Resend email validation link" msgid "Resend email validation link"
msgstr "Renvoyer le lien de validation" msgstr "Renvoyer le lien de validation"
#: apps/registration/views.py:174 #: apps/registration/views.py:178
msgid "Pre-registered users list" msgid "Pre-registered users list"
msgstr "Liste des utilisateurs en attente d'inscription" msgstr "Liste des utilisateurs en attente d'inscription"
#: apps/registration/views.py:198 #: apps/registration/views.py:202
msgid "Unregistered users" msgid "Unregistered users"
msgstr "Utilisateurs en attente d'inscription" msgstr "Utilisateurs en attente d'inscription"
#: apps/registration/views.py:211 #: apps/registration/views.py:215
msgid "Registration detail" msgid "Registration detail"
msgstr "Détails de l'inscription" msgstr "Détails de l'inscription"
@ -2225,7 +2280,7 @@ msgstr "Cette facture est verrouillée et ne peut pas être supprimée."
msgid "" msgid ""
"Are you sure you want to delete this invoice? This action can't be undone." "Are you sure you want to delete this invoice? This action can't be undone."
msgstr "" msgstr ""
"Êtes-vous sûr de vouloir supprimer cette facture ? Cette action ne pourra " "Êtes-vous sûr⋅e de vouloir supprimer cette facture ? Cette action ne pourra "
"pas être annulée." "pas être annulée."
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:28 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:28
@ -2863,7 +2918,7 @@ msgid ""
"Are you sure you want to delete the registration of %(user)s for the WEI " "Are you sure you want to delete the registration of %(user)s for the WEI "
"%(wei_name)s? This action can't be undone." "%(wei_name)s? This action can't be undone."
msgstr "" msgstr ""
"Êtes-vous sûr de vouloir supprimer l'inscription de %(user)s pour le WEI " "Êtes-vous sûr⋅e de vouloir supprimer l'inscription de %(user)s pour le WEI "
"%(wei_name)s ? Cette action ne pourra pas être annulée." "%(wei_name)s ? Cette action ne pourra pas être annulée."
#: apps/wei/templates/wei/weiregistration_list.html:19 #: apps/wei/templates/wei/weiregistration_list.html:19
@ -3127,6 +3182,113 @@ msgstr "Chercher par un attribut tel que le nom …"
msgid "There is no results." msgid "There is no results."
msgstr "Il n'y a pas de résultat." msgstr "Il n'y a pas de résultat."
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8
msgid "Are you sure to delete the application"
msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application"
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:17
#: note_kfet/templates/oauth2_provider/authorize.html:28
msgid "Cancel"
msgstr "Annuler"
#: note_kfet/templates/oauth2_provider/application_detail.html:11
msgid "Client id"
msgstr "ID client"
#: note_kfet/templates/oauth2_provider/application_detail.html:14
msgid "Client secret"
msgstr "Secret client"
#: note_kfet/templates/oauth2_provider/application_detail.html:17
msgid "Client type"
msgstr "Type de client"
#: note_kfet/templates/oauth2_provider/application_detail.html:20
msgid "Authorization Grant Type"
msgstr "Type d'autorisation"
#: note_kfet/templates/oauth2_provider/application_detail.html:23
msgid "Redirect Uris"
msgstr "URIs de redirection"
#: note_kfet/templates/oauth2_provider/application_detail.html:29
#, python-format
msgid ""
"You can go <a href=\"%(scopes_url)s\">here</a> to generate authorization "
"link templates and convert permissions to scope numbers with the permissions "
"that you want to grant for your application."
msgstr ""
"Vous pouvez aller <a href=\"%(scopes_url)s\">ici</a> pour générer des modèles "
"de liens d'autorisation et convertir des permissions en identifiants de "
"scopes avec les permissions que vous souhaitez attribuer à votre application."
#: note_kfet/templates/oauth2_provider/application_detail.html:37
#: note_kfet/templates/oauth2_provider/application_form.html:23
msgid "Go Back"
msgstr "Retour en arrière"
#: note_kfet/templates/oauth2_provider/application_form.html:12
msgid "Edit application"
msgstr "Modifier l'application"
#: note_kfet/templates/oauth2_provider/application_list.html:7
msgid "Your applications"
msgstr "Vos applications"
#: note_kfet/templates/oauth2_provider/application_list.html:11
msgid ""
"You can find on this page the list of the applications that you already "
"registered."
msgstr ""
"Vous pouvez trouver sur cette page la liste des applications que vous avez "
"déjà enregistrées."
#: note_kfet/templates/oauth2_provider/application_list.html:30
msgid "New Application"
msgstr "Nouvelle application"
#: note_kfet/templates/oauth2_provider/application_list.html:31
msgid "Authorized Tokens"
msgstr "Jetons autorisés"
#: note_kfet/templates/oauth2_provider/application_registration_form.html:5
msgid "Register a new application"
msgstr "Enregistrer une nouvelle application"
#: note_kfet/templates/oauth2_provider/authorize.html:9
#: note_kfet/templates/oauth2_provider/authorize.html:29
msgid "Authorize"
msgstr "Autoriser"
#: note_kfet/templates/oauth2_provider/authorize.html:14
msgid "Application requires following permissions:"
msgstr "L'application requiert les permissions suivantes :"
#: note_kfet/templates/oauth2_provider/authorize.html:36
#: note_kfet/templates/oauth2_provider/authorized-oob.html:15
msgid "Error:"
msgstr "Erreur :"
#: note_kfet/templates/oauth2_provider/authorized-oob.html:13
msgid "Success"
msgstr "Succès"
#: note_kfet/templates/oauth2_provider/authorized-oob.html:21
msgid "Please return to your application and enter this code:"
msgstr "Merci de retourner à votre application et entrez ce code :"
#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:9
msgid "Are you sure you want to delete this token?"
msgstr "Êtes-vous sûr⋅e de vouloir supprimer ce jeton ?"
#: note_kfet/templates/oauth2_provider/authorized-tokens.html:7
msgid "Tokens"
msgstr "Jetons"
#: note_kfet/templates/oauth2_provider/authorized-tokens.html:22
msgid "There are no authorized tokens yet."
msgstr "Il n'y a pas encore de jeton autorisé."
#: note_kfet/templates/registration/logged_out.html:13 #: note_kfet/templates/registration/logged_out.html:13
msgid "Thanks for spending some quality time with the Web site today." msgid "Thanks for spending some quality time with the Web site today."
msgstr "Merci d'avoir utilisé la Note Kfet." msgstr "Merci d'avoir utilisé la Note Kfet."
@ -3168,7 +3330,7 @@ msgid ""
"password twice so we can verify you typed it in correctly." "password twice so we can verify you typed it in correctly."
msgstr "" msgstr ""
"Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis " "Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis "
"renseigner votre nouveau mot de passe à deux reprises, pour être sûr de " "renseigner votre nouveau mot de passe à deux reprises, pour être sûr⋅e de "
"l'avoir tapé correctement." "l'avoir tapé correctement."
#: note_kfet/templates/registration/password_change_form.html:16 #: note_kfet/templates/registration/password_change_form.html:16

View File

@ -6,7 +6,6 @@ from django.contrib.admin import AdminSite
from django.contrib.sites.admin import Site, SiteAdmin from django.contrib.sites.admin import Site, SiteAdmin
from member.views import CustomLoginView from member.views import CustomLoginView
from .middlewares import get_current_session
class StrongAdminSite(AdminSite): class StrongAdminSite(AdminSite):
@ -14,8 +13,7 @@ class StrongAdminSite(AdminSite):
""" """
Authorize only staff that have the correct permission mask Authorize only staff that have the correct permission mask
""" """
session = get_current_session() return request.user.is_active and request.user.is_staff and request.session.get("permission_mask", -1) >= 42
return request.user.is_active and request.user.is_staff and session.get("permission_mask", -1) >= 42
def login(self, request, extra_context=None): def login(self, request, extra_context=None):
return CustomLoginView.as_view()(request) return CustomLoginView.as_view()(request)

View File

@ -1,43 +1,23 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.sessions.backends.db import SessionStore
from threading import local from threading import local
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') from django.conf import settings
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') from django.contrib.auth import login
IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') from django.contrib.auth.models import User
REQUEST_ATTR_NAME = getattr(settings, 'LOCAL_REQUEST_ATTR_NAME', '_current_request')
_thread_locals = local() _thread_locals = local()
def _set_current_user_and_ip(user=None, session=None, ip=None): def _set_current_request(request=None):
setattr(_thread_locals, USER_ATTR_NAME, user) setattr(_thread_locals, REQUEST_ATTR_NAME, request)
setattr(_thread_locals, SESSION_ATTR_NAME, session)
setattr(_thread_locals, IP_ATTR_NAME, ip)
def get_current_user() -> User: def get_current_request():
return getattr(_thread_locals, USER_ATTR_NAME, None) return getattr(_thread_locals, REQUEST_ATTR_NAME, None)
def get_current_session() -> SessionStore:
return getattr(_thread_locals, SESSION_ATTR_NAME, None)
def get_current_ip() -> str:
return getattr(_thread_locals, IP_ATTR_NAME, None)
def get_current_authenticated_user():
current_user = get_current_user()
if isinstance(current_user, AnonymousUser):
return None
return current_user
class SessionMiddleware(object): class SessionMiddleware(object):
@ -49,8 +29,6 @@ class SessionMiddleware(object):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
user = request.user
# If we authenticate through a token to connect to the API, then we query the good user # If we authenticate through a token to connect to the API, then we query the good user
if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"): if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"):
token = request.META.get('HTTP_AUTHORIZATION') token = request.META.get('HTTP_AUTHORIZATION')
@ -60,20 +38,14 @@ class SessionMiddleware(object):
if Token.objects.filter(key=token).exists(): if Token.objects.filter(key=token).exists():
token_obj = Token.objects.get(key=token) token_obj = Token.objects.get(key=token)
user = token_obj.user user = token_obj.user
request.user = user
session = request.session session = request.session
session["permission_mask"] = 42 session["permission_mask"] = 42
session.save() session.save()
if 'HTTP_X_REAL_IP' in request.META: _set_current_request(request)
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')
_set_current_user_and_ip(user, request.session, ip)
response = self.get_response(request) response = self.get_response(request)
_set_current_user_and_ip(None, None, None) _set_current_request(None)
return response return response

View File

@ -245,6 +245,11 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 20, 'PAGE_SIZE': 20,
} }
# OAuth2 Provider
OAUTH2_PROVIDER = {
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
}
# Take control on how widget templates are sourced # Take control on how widget templates are sourced
# See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting # See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Are you sure to delete the application" %} {{ application.name }}?</h3>
</div>
<div class="card-footer text-center">
<form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
{% csrf_token %}
<div class="control-group">
<div class="controls">
<a class="btn btn-secondary btn-large" href="{% url "oauth2_provider:list" %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-large btn-danger" name="allow" value="{% trans "Delete" %}"/>
</div>
</div>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3>{{ application.name }}</h3>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6">{% trans "Client id" %}</dt>
<dd class="col-xl-6"><input class="form-control" type="text" value="{{ application.client_id }}" readonly></dd>
<dt class="col-xl-6">{% trans "Client secret" %}</dt>
<dd class="col-xl-6"><input class="form-control" type="text" value="****************************************************************" readonly></dd>
<dt class="col-xl-6">{% trans "Client type" %}</dt>
<dd class="col-xl-6">{{ application.client_type }}</dd>
<dt class="col-xl-6">{% trans "Authorization Grant Type" %}</dt>
<dd class="col-xl-6">{{ application.authorization_grant_type }}</dd>
<dt class="col-xl-6">{% trans "Redirect Uris" %}</dt>
<dd class="col-xl-6"><textarea class="form-control" readonly>{{ application.redirect_uris }}</textarea></dd>
</dl>
<div class="alert alert-info">
{% url 'permission:scopes' as scopes_url %}
{% blocktrans trimmed %}
You can go <a href="{{ scopes_url }}">here</a> to generate authorization link templates and convert
permissions to scope numbers with the permissions that you want to grant for your application.
{% endblocktrans %}
</div>
</div>
<div class="card-footer text-center">
<a class="btn btn-secondary" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
<a class="btn btn-primary" href="{% url "oauth2_provider:update" application.id %}">{% trans "Edit" %}</a>
<a class="btn btn-danger" href="{% url "oauth2_provider:delete" application.id %}">{% trans "Delete" %}</a>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block content %}
<form class="form-horizontal" method="post" action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.id %}{% endblock app-form-action-url %}">
<div class="card">
<div class="card-header text-center">
<h3 class="block-center-heading">
{% block app-form-title %}
{% trans "Edit application" %} {{ application.name }}
{% endblock app-form-title %}
</h3>
</div>
<div class="card-body">
{% csrf_token %}
{{ form|crispy }}
</div>
<div class="card-footer text-center control-group">
<div class="controls">
<a class="btn btn-secondary" href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.id %}{% endblock app-form-back-url %}">
{% trans "Go Back" %}
</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Your applications" %}</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
{% blocktrans trimmed %}
You can find on this page the list of the applications that you already registered.
{% endblocktrans %}
</div>
{% if applications %}
<ul>
{% for application in applications %}
<li><a href="{{ application.get_absolute_url }}">{{ application.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<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>
{% endif %}
</div>
<div class="card-footer text-center">
<a class="btn btn-success" href="{% url "oauth2_provider:register" %}">{% trans "New Application" %}</a>
<a class="btn btn-secondary" href="{% url "oauth2_provider:authorized-token-list" %}">{% trans "Authorized Tokens" %}</a>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends "oauth2_provider/application_form.html" %}
{% load i18n %}
{% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %}
{% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %}
{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %}

View File

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Authorize" %} {{ application.name }} ?</h3>
</div>
{% if not error %}
<form id="authorizationForm" method="post">
<div class="card-body">
<p>{% trans "Application requires following permissions:" %}</p>
<ul>
{% for scope in scopes_descriptions %}
<li>{{ scope }}</li>
{% endfor %}
</ul>
{% csrf_token %}
{{ form|crispy }}
</div>
<div class="card-footer text-center">
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-large btn-danger" value="{% trans "Cancel" %}"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
</div>
</div>
</div>
</form>
{% else %}
<div class="card-body">
<h2>{% trans "Error:" %} {{ error.error }}</h2>
<p>{{ error.description }}</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
{# Small hack to have the remove the allow checkbox and replace it with the button #}
{# Django oauth toolkit does simply not render the wdiget since it is not hidden, and create directly the button #}
document.getElementById('div_id_allow').parentElement.remove()
</script>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
Success code={{code}}
{% endblock %}
{% block content %}
<div class="card">
<h3 class="card-header text-center">
{% if not error %}
{% trans "Success" %}
{% else %}
{% trans "Error:" %} {{ error.error }}
{% endif %}
</h3>
<div class="card-body">
{% if not error %}
<p>{% trans "Please return to your application and enter this code:" %}</p>
<p><code>{{ code }}</code></p>
{% else %}
<p>{{ error.description }}</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<form action="" method="post">
{% csrf_token %}
<h3 class="card-header text-center">
{% trans "Are you sure you want to delete this token?" %}
</h3>
<div class="card-footer text-center">
<input type="submit" value="{% trans "Delete" %}" />
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card">
<h3 class="card-header text-center">
{% trans "Tokens" %}
</h3>
<div class="card-body">
<ul>
{% for authorized_token in authorized_tokens %}
<li>
{{ authorized_token.application }}
(<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">revoke</a>)
</li>
<ul>
{% for scope_name, scope_description in authorized_token.scopes.items %}
<li>{{ scope_name }}: {{ scope_description }}</li>
{% endfor %}
</ul>
{% empty %}
<li>{% trans "There are no authorized tokens yet." %}</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -19,11 +19,11 @@ class IndexView(LoginRequiredMixin, RedirectView):
user = self.request.user user = self.request.user
# The account note will have the consumption page as default page # The account note will have the consumption page as default page
if not PermissionBackend.check_perm(user, "auth.view_user", user): if not PermissionBackend.check_perm(self.request, "auth.view_user", user):
return reverse("note:consos") return reverse("note:consos")
# People that can see the alias BDE are Kfet members # People that can see the alias BDE are Kfet members
if PermissionBackend.check_perm(user, "alias.view_alias", Alias.objects.get(name="BDE")): if PermissionBackend.check_perm(self.request, "alias.view_alias", Alias.objects.get(name="BDE")):
return reverse("note:transfer") return reverse("note:transfer")
# Non-Kfet members will don't see the transfer page, but their profile page # Non-Kfet members will don't see the transfer page, but their profile page