diff --git a/elections/migrations/0031_alter_election_options.py b/elections/migrations/0031_alter_election_options.py new file mode 100644 index 0000000..7cfe5ed --- /dev/null +++ b/elections/migrations/0031_alter_election_options.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.4 on 2021-07-12 16:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("elections", "0030_timestamps"), + ] + + operations = [ + migrations.AlterModelOptions( + name="election", + options={ + "ordering": ["-start_date", "-end_date"], + "permissions": [("election_admin", "Peut administrer des élections")], + }, + ), + ] diff --git a/elections/mixins.py b/elections/mixins.py index d4ac4e6..2b243bb 100644 --- a/elections/mixins.py +++ b/elections/mixins.py @@ -10,7 +10,7 @@ from .models import Election, Option, Question class AdminOnlyMixin(PermissionRequiredMixin): """Restreint l'accès aux admins""" - permission_required = "elections.is_admin" + permission_required = "elections.election_admin" class SelectElectionMixin: diff --git a/elections/models.py b/elections/models.py index 0fb2b79..f99eda7 100644 --- a/elections/models.py +++ b/elections/models.py @@ -6,12 +6,12 @@ from django.db import models, transaction from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ +from shared.auth import CONNECTION_METHODS from shared.utils import choices_length from .staticdefs import ( BALLOT_TYPE, CAST_FUNCTIONS, - CONNECTION_METHODS, QUESTION_TYPES, TALLY_FUNCTIONS, VALIDATE_FUNCTIONS, @@ -81,7 +81,7 @@ class Election(models.Model): class Meta: permissions = [ - ("is_admin", _("Peut administrer des élections")), + ("election_admin", _("Peut administrer des élections")), ] ordering = ["-start_date", "-end_date"] diff --git a/elections/staticdefs.py b/elections/staticdefs.py index df6b35e..d2389af 100644 --- a/elections/staticdefs.py +++ b/elections/staticdefs.py @@ -22,11 +22,6 @@ MAIL_VOTE_DELETED = ( "Kadenios" ) -CONNECTION_METHODS = { - "pwd": _("mot de passe"), - "cas": _("CAS"), -} - QUESTION_TYPES = [ ("assentiment", _("Assentiment")), ("uninominal", _("Uninominal")), diff --git a/faqs/migrations/0002_alter_faq_options.py b/faqs/migrations/0002_alter_faq_options.py new file mode 100644 index 0000000..562e62b --- /dev/null +++ b/faqs/migrations/0002_alter_faq_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.4 on 2021-07-12 17:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("faqs", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="faq", + options={"permissions": [("faq_admin", "Can create faqs")]}, + ), + ] diff --git a/faqs/mixins.py b/faqs/mixins.py index 8a09157..d117348 100644 --- a/faqs/mixins.py +++ b/faqs/mixins.py @@ -4,7 +4,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin class AdminOnlyMixin(PermissionRequiredMixin): """Restreint l'accès aux admins""" - permission_required = "faqs.is_author" + permission_required = "faqs.faq_admin" class CreatorOnlyMixin(AdminOnlyMixin): diff --git a/faqs/models.py b/faqs/models.py index f972c61..88ae1d4 100644 --- a/faqs/models.py +++ b/faqs/models.py @@ -25,7 +25,7 @@ class Faq(models.Model): class Meta: permissions = [ - ("is_author", "Can create faqs"), + ("faq_admin", "Can create faqs"), ] constraints = [ models.UniqueConstraint(fields=["anchor"], name="unique_faq_anchor") diff --git a/shared/auth/__init__.py b/shared/auth/__init__.py new file mode 100644 index 0000000..074cc4f --- /dev/null +++ b/shared/auth/__init__.py @@ -0,0 +1,3 @@ +from .staticdefs import CONNECTION_METHODS + +__all__ = [CONNECTION_METHODS] diff --git a/shared/auth/forms.py b/shared/auth/forms.py index feda82c..00b5204 100644 --- a/shared/auth/forms.py +++ b/shared/auth/forms.py @@ -82,3 +82,38 @@ class PwdUserForm(forms.ModelForm): class Meta: model = User fields = ["username", "full_name", "email"] + + +class UserAdminForm(forms.Form): + """ + Allows to select an user and give them some admin permissions + """ + + username = forms.CharField(label=_("Nom d'utilisateur"), max_length=150) + + full_admin = forms.BooleanField( + label=_("Passer administrateur de Kadenios"), required=False + ) + faq_admin = forms.BooleanField( + label=_("Autoriser à créer des FAQs"), required=False + ) + election_admin = forms.BooleanField( + label=_("Autoriser à créer des élections"), required=False + ) + + def clean(self): + cleaned_data = super().clean() + username = cleaned_data["username"] + + if not username[:5] in ["cas__", "pwd__"]: + self.add_error( + "username", + _( + "Format de login invalide, seuls les comptes CAS ou avec " + "mot de passe sont modifiables" + ), + ) + elif not User.objects.filter(username=username).exists(): + self.add_error("username", _("Pas d'utilisateur·rice avec ce login")) + + return cleaned_data diff --git a/shared/auth/staticdefs.py b/shared/auth/staticdefs.py new file mode 100644 index 0000000..70400f1 --- /dev/null +++ b/shared/auth/staticdefs.py @@ -0,0 +1,6 @@ +from django.utils.translation import gettext_lazy as _ + +CONNECTION_METHODS = { + "pwd": _("mot de passe"), + "cas": _("CAS"), +} diff --git a/shared/auth/urls.py b/shared/auth/urls.py index d110dc9..e3e15ff 100644 --- a/shared/auth/urls.py +++ b/shared/auth/urls.py @@ -9,4 +9,9 @@ urlpatterns = [ name="auth.election", ), path("pwd-create", views.CreatePwdAccount.as_view(), name="auth.create-account"), + path("admin", views.AdminPanelView.as_view(), name="auth.admin"), + path( + "permissions", views.PermissionManagementView.as_view(), name="auth.permissions" + ), + path("accounts", views.AccountListView.as_view(), name="auth.accounts"), ] diff --git a/shared/auth/views.py b/shared/auth/views.py index 5cee235..258ecef 100644 --- a/shared/auth/views.py +++ b/shared/auth/views.py @@ -1,16 +1,34 @@ -from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth import get_user_model from django.contrib.auth import views as auth_views from django.contrib.auth.hashers import make_password -from django.urls import reverse_lazy -from django.utils.decorators import method_decorator -from django.views.generic.edit import CreateView +from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.models import Permission +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, FormView, ListView, TemplateView -from .forms import ElectionAuthForm, PwdUserForm +from .forms import ElectionAuthForm, PwdUserForm, UserAdminForm from .utils import generate_password User = get_user_model() + +# ############################################################################# +# Mixin to restrict access to staff members +# ############################################################################# + + +class StaffMemberMixin(UserPassesTestMixin): + """ + Mixin permettant de restreindre l'accès aux membres `staff`, si la personne + n'est pas connectée, renvoie sur la page d'authentification + """ + + def test_func(self): + return self.request.user.is_active and self.request.user.is_staff + + # ############################################################################# # Election Specific Login # ############################################################################# @@ -28,13 +46,21 @@ class ElectionLoginView(auth_views.LoginView): return super().get_context_data(**kwargs) +# ############################################################################# +# Admin Panel +# ############################################################################# + + +class AdminPanelView(StaffMemberMixin, TemplateView): + template_name = "auth/admin-panel.html" + + # ############################################################################# # Creation of Password Accounts # ############################################################################# -@method_decorator(staff_member_required, name="dispatch") -class CreatePwdAccount(CreateView): +class CreatePwdAccount(StaffMemberMixin, CreateView): model = User form_class = PwdUserForm template_name = "auth/create-user.html" @@ -46,3 +72,79 @@ class CreatePwdAccount(CreateView): # On envoie un mail pour réinitialiser le mot de passe return super().form_valid(form) + + +# ############################################################################# +# List of password and CAS users +# ############################################################################# + + +class AccountListView(StaffMemberMixin, ListView): + model = User + template_name = "auth/account-list.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + qs = self.get_queryset() + + ctx["cas_users"] = qs.filter(username__startswith="cas__") + ctx["pwd_users"] = qs.filter(username__startswith="pwd__") + + return ctx + + +# ############################################################################# +# Permission management +# ############################################################################# + + +class PermissionManagementView(StaffMemberMixin, SuccessMessageMixin, FormView): + form_class = UserAdminForm + template_name = "auth/permission-management.html" + success_message = _("Permissions modifiées avec succès !") + + def get_context_data(self, **kwargs): + kwargs.update({"username": self.request.GET.get("user", None)}) + return super().get_context_data(**kwargs) + + def get_initial(self): + username = self.request.GET.get("user", None) + if username is not None: + user = User.objects.filter(username=username).first() + + if user is not None: + return { + "username": username, + "full_admin": user.is_staff, + "election_admin": user.has_perm("elections.election_admin"), + "faq_admin": user.has_perm("faqs.faq_admin"), + } + + return {} + + def get_success_url(self): + return reverse("auth.permissions") + f"?user={self.user}" + + def form_valid(self, form): + user = User.objects.get(username=form.cleaned_data["username"]) + self.user = user.username + + # Kadenios admin + user.is_staff = form.cleaned_data["full_admin"] + + # Election admin + perm_election = Permission.objects.get(codename="election_admin") + if form.cleaned_data["election_admin"]: + perm_election.user_set.add(user) + else: + perm_election.user_set.remove(user) + + # FAQ admin + perm_faq = Permission.objects.get(codename="faq_admin") + if form.cleaned_data["faq_admin"]: + perm_faq.user_set.add(user) + else: + perm_faq.user_set.remove(user) + + user.save() + return super().form_valid(form) diff --git a/shared/templates/auth/account-list.html b/shared/templates/auth/account-list.html new file mode 100644 index 0000000..df788a8 --- /dev/null +++ b/shared/templates/auth/account-list.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block extra_head %} + +{% endblock %} + + +{% block content %} + +
{% trans "Comptes avec mot de passe" %}
+ + {# Search bar #} +{% trans "Comptes CAS" %}
+ + {# Search bar #} +