diff --git a/bda/views.py b/bda/views.py index d6da2e9f..84b6c9d3 100644 --- a/bda/views.py +++ b/bda/views.py @@ -782,9 +782,9 @@ def catalogue(request, request_type): .select_related('location') .prefetch_related('quote_set') ) - if categories_id: + if categories_id and 0 not in categories_id: shows_qs = shows_qs.filter(category__id__in=categories_id) - if locations_id: + if locations_id and 0 not in locations_id: shows_qs = shows_qs.filter(location__id__in=locations_id) # On convertit les descriptions à envoyer en une liste facilement diff --git a/cof/settings/common.py b/cof/settings/common.py index ba0b6044..f92dc83b 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -45,6 +45,7 @@ RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") +LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") BASE_DIR = os.path.dirname( @@ -90,6 +91,7 @@ INSTALLED_APPS = [ 'wagtailmenus', 'modelcluster', 'taggit', + 'kfet.auth', 'kfet.cms', ] @@ -99,7 +101,7 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'kfet.middleware.KFetAuthenticationMiddleware', + 'kfet.auth.middleware.TemporaryAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -127,7 +129,7 @@ TEMPLATES = [ 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', - 'kfet.context_processors.auth', + 'kfet.auth.context_processors.temporary_auth', 'kfet.context_processors.config', ], }, @@ -190,7 +192,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'gestioncof.shared.COFCASBackend', - 'kfet.backends.GenericTeamBackend', + 'kfet.auth.backends.GenericBackend', ) RECAPTCHA_USE_SSL = True diff --git a/gestioncof/views.py b/gestioncof/views.py index c5701510..ec9f6efd 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -577,7 +577,7 @@ def export_members(request): writer = unicodecsv.writer(response) for profile in CofProfile.objects.filter(is_cof=True).all(): user = profile.user - bits = [profile.id, user.username, user.first_name, user.last_name, + bits = [user.id, user.username, user.first_name, user.last_name, user.email, profile.phone, profile.occupation, profile.departement, profile.type_cotiz] writer.writerow([str(bit) for bit in bits]) @@ -596,7 +596,7 @@ def csv_export_mega(filename, qs): comments = "---".join( [comment.content for comment in reg.comments.all()]) bits = [user.username, user.first_name, user.last_name, user.email, - profile.phone, profile.id, + profile.phone, user.id, profile.comments if profile.comments else "", comments] writer.writerow([str(bit) for bit in bits]) diff --git a/kfet/apps.py b/kfet/apps.py index 3dd2c0e8..4f114c37 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -11,7 +11,6 @@ class KFetConfig(AppConfig): verbose_name = "Application K-Fêt" def ready(self): - import kfet.signals self.register_config() def register_config(self): diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py new file mode 100644 index 00000000..00926030 --- /dev/null +++ b/kfet/auth/__init__.py @@ -0,0 +1,4 @@ +default_app_config = 'kfet.auth.apps.KFetAuthConfig' + +KFET_GENERIC_USERNAME = 'kfet_genericteam' +KFET_GENERIC_TRIGRAMME = 'GNR' diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py new file mode 100644 index 00000000..d91931f5 --- /dev/null +++ b/kfet/auth/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig +from django.db.models.signals import post_migrate +from django.utils.translation import ugettext_lazy as _ + + +class KFetAuthConfig(AppConfig): + name = 'kfet.auth' + label = 'kfetauth' + verbose_name = _("K-Fêt - Authentification et Autorisation") + + def ready(self): + from . import signals # noqa + from .utils import setup_kfet_generic_user + post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py new file mode 100644 index 00000000..c6ad21b2 --- /dev/null +++ b/kfet/auth/backends.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth import get_user_model +from kfet.models import Account, GenericTeamToken + +from .utils import get_kfet_generic_user + +User = get_user_model() + + +class BaseKFetBackend: + def get_user(self, user_id): + """ + Add extra select related up to Account. + """ + try: + return ( + User.objects + .select_related('profile__account_kfet') + .get(pk=user_id) + ) + except User.DoesNotExist: + return None + + +class AccountBackend(BaseKFetBackend): + def authenticate(self, request, kfet_password=None): + try: + return Account.objects.get_by_password(kfet_password).user + except Account.DoesNotExist: + return None + + +class GenericBackend(BaseKFetBackend): + def authenticate(self, request, kfet_token=None): + try: + team_token = GenericTeamToken.objects.get(token=kfet_token) + except GenericTeamToken.DoesNotExist: + return + + # No need to keep the token. + team_token.delete() + + return get_kfet_generic_user() diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py new file mode 100644 index 00000000..7b59b88b --- /dev/null +++ b/kfet/auth/context_processors.py @@ -0,0 +1,10 @@ +from django.contrib.auth.context_processors import PermWrapper + + +def temporary_auth(request): + if hasattr(request, 'real_user'): + return { + 'user': request.real_user, + 'perms': PermWrapper(request.real_user), + } + return {} diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py new file mode 100644 index 00000000..28ba1c9e --- /dev/null +++ b/kfet/auth/fields.py @@ -0,0 +1,20 @@ +from django import forms +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.forms import widgets + + +class KFetPermissionsField(forms.ModelMultipleChoiceField): + + def __init__(self, *args, **kwargs): + queryset = Permission.objects.filter( + content_type__in=ContentType.objects.filter(app_label="kfet"), + ) + super().__init__( + queryset=queryset, + widget=widgets.CheckboxSelectMultiple, + *args, **kwargs + ) + + def label_from_instance(self, obj): + return obj.name diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py new file mode 100644 index 00000000..0c9fa53b --- /dev/null +++ b/kfet/auth/forms.py @@ -0,0 +1,44 @@ +from django import forms +from django.contrib.auth.models import Group, User + +from .fields import KFetPermissionsField + + +class GroupForm(forms.ModelForm): + permissions = KFetPermissionsField() + + def clean_name(self): + name = self.cleaned_data['name'] + return 'K-Fêt %s' % name + + def clean_permissions(self): + kfet_perms = self.cleaned_data['permissions'] + # TODO: With Django >=1.11, the QuerySet method 'difference' can be + # used. + # other_groups = self.instance.permissions.difference( + # self.fields['permissions'].queryset + # ) + other_perms = self.instance.permissions.exclude( + pk__in=[p.pk for p in self.fields['permissions'].queryset], + ) + return list(kfet_perms) + list(other_perms) + + class Meta: + model = Group + fields = ['name', 'permissions'] + + +class UserGroupForm(forms.ModelForm): + groups = forms.ModelMultipleChoiceField( + Group.objects.filter(name__icontains='K-Fêt'), + label='Statut équipe', + required=False) + + def clean_groups(self): + kfet_groups = self.cleaned_data.get('groups') + other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') + return list(kfet_groups) + list(other_groups) + + class Meta: + model = User + fields = ['groups'] diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py new file mode 100644 index 00000000..748ce4dd --- /dev/null +++ b/kfet/auth/middleware.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth import get_user_model + +from .backends import AccountBackend + +User = get_user_model() + + +class TemporaryAuthMiddleware: + """Authenticate another user for this request if AccountBackend succeeds. + + By the way, if a user is authenticated, we refresh its from db to add + values from CofProfile and Account of this user. + + """ + def process_request(self, request): + if request.user.is_authenticated(): + # avoid multiple db accesses in views and templates + request.user = ( + User.objects + .select_related('profile__account_kfet') + .get(pk=request.user.pk) + ) + + temp_request_user = AccountBackend().authenticate( + request, + kfet_password=self.get_kfet_password(request), + ) + + if temp_request_user: + request.real_user = request.user + request.user = temp_request_user + + def get_kfet_password(self, request): + return ( + request.META.get('HTTP_KFETPASSWORD') or + request.POST.get('KFETPASSWORD') + ) diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py new file mode 100644 index 00000000..061570a8 --- /dev/null +++ b/kfet/auth/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0006_require_contenttypes_0002'), + # Following dependency allows using Account model to set up the kfet + # generic user in post_migrate receiver. + ('kfet', '0058_delete_genericteamtoken'), + ] + + operations = [ + migrations.CreateModel( + name='GenericTeamToken', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), + ('token', models.CharField(unique=True, max_length=50)), + ], + ), + ] diff --git a/kfet/auth/migrations/__init__.py b/kfet/auth/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kfet/auth/models.py b/kfet/auth/models.py new file mode 100644 index 00000000..ecd40091 --- /dev/null +++ b/kfet/auth/models.py @@ -0,0 +1,17 @@ +from django.db import models +from django.utils.crypto import get_random_string + + +class GenericTeamTokenManager(models.Manager): + + def create_token(self): + token = get_random_string(50) + while self.filter(token=token).exists(): + token = get_random_string(50) + return self.create(token=token) + + +class GenericTeamToken(models.Model): + token = models.CharField(max_length=50, unique=True) + + objects = GenericTeamTokenManager() diff --git a/kfet/auth/signals.py b/kfet/auth/signals.py new file mode 100644 index 00000000..3d7af18b --- /dev/null +++ b/kfet/auth/signals.py @@ -0,0 +1,40 @@ +from django.contrib import messages +from django.contrib.auth.signals import user_logged_in +from django.core.urlresolvers import reverse +from django.dispatch import receiver +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from .utils import get_kfet_generic_user + + +@receiver(user_logged_in) +def suggest_auth_generic(sender, request, user, **kwargs): + """ + Suggest logged in user to continue as the kfet generic user. + + Message is only added if the following conditions are met: + - the next page (where user is going to be redirected due to successful + authentication) is related to kfet, i.e. 'k-fet' is in its url. + - logged in user is a kfet staff member (except the generic user). + """ + # Filter against the next page. + if not(hasattr(request, 'GET') and 'next' in request.GET): + return + + next_page = request.GET['next'] + generic_url = reverse('kfet.login.generic') + + if not('k-fet' in next_page and not next_page.startswith(generic_url)): + return + + # Filter against the logged in user. + if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()): + return + + # Seems legit to add message. + text = _("K-Fêt — Ouvrir une session partagée ?") + messages.info(request, mark_safe( + '{}' + .format(generic_url, text) + )) diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py new file mode 100644 index 00000000..c2f183cd --- /dev/null +++ b/kfet/auth/tests.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +from unittest import mock + +from django.core import signing +from django.core.urlresolvers import reverse +from django.contrib.auth.models import AnonymousUser, Group, Permission, User +from django.test import RequestFactory, TestCase + +from kfet.forms import UserGroupForm +from kfet.models import Account + +from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME +from .backends import AccountBackend, GenericBackend +from .middleware import TemporaryAuthMiddleware +from .models import GenericTeamToken +from .utils import get_kfet_generic_user +from .views import GenericLoginView + + +## +# Forms +## + +class UserGroupFormTests(TestCase): + """Test suite for UserGroupForm.""" + + def setUp(self): + # create user + self.user = User.objects.create(username="foo", password="foo") + + # create some K-Fêt groups + prefix_name = "K-Fêt " + names = ["Group 1", "Group 2", "Group 3"] + self.kfet_groups = [ + Group.objects.create(name=prefix_name+name) + for name in names + ] + + # create a non-K-Fêt group + self.other_group = Group.objects.create(name="Other group") + + def test_choices(self): + """Only K-Fêt groups are selectable.""" + form = UserGroupForm(instance=self.user) + groups_field = form.fields['groups'] + self.assertQuerysetEqual( + groups_field.queryset, + [repr(g) for g in self.kfet_groups], + ordered=False, + ) + + def test_keep_others(self): + """User stays in its non-K-Fêt groups.""" + user = self.user + + # add user to a non-K-Fêt group + user.groups.add(self.other_group) + + # add user to some K-Fêt groups through UserGroupForm + data = { + 'groups': [group.pk for group in self.kfet_groups], + } + form = UserGroupForm(data, instance=user) + + form.is_valid() + form.save() + self.assertQuerysetEqual( + user.groups.all(), + [repr(g) for g in [self.other_group] + self.kfet_groups], + ordered=False, + ) + + +class KFetGenericUserTests(TestCase): + + def test_exists(self): + """ + The account is set up when app is ready, so it should exist. + """ + generic = Account.objects.get_generic() + self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME) + self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME) + self.assertEqual(get_kfet_generic_user(), generic.user) + + +## +# Backends +## + +class AccountBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + acc = Account(trigramme='000') + acc.change_pwd('valid') + acc.save({'username': 'user'}) + + auth = AccountBackend().authenticate( + self.request, kfet_password='valid') + + self.assertEqual(auth, acc.user) + + def test_invalid(self): + auth = AccountBackend().authenticate( + self.request, kfet_password='invalid') + self.assertIsNone(auth) + + +class GenericBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + token = GenericTeamToken.objects.create_token() + + auth = GenericBackend().authenticate( + self.request, kfet_token=token.token) + + self.assertEqual(auth, get_kfet_generic_user()) + self.assertEqual(GenericTeamToken.objects.all().count(), 0) + + def test_invalid(self): + auth = GenericBackend().authenticate( + self.request, kfet_token='invalid') + self.assertIsNone(auth) + + +## +# Views +## + +class GenericLoginViewTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + user_acc = Account(trigramme='000') + user_acc.save({'username': 'user'}) + self.user = user_acc.user + self.user.set_password('user') + self.user.save() + + team_acc = Account(trigramme='100') + team_acc.save({'username': 'team'}) + self.team = team_acc.user + self.team.set_password('team') + self.team.save() + self.team.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', codename='is_team'), + ) + + self.url = reverse('kfet.login.generic') + self.generic_user = get_kfet_generic_user() + + def test_url(self): + self.assertEqual(self.url, '/k-fet/login/generic') + + def test_notoken_get(self): + """ + Send confirmation for user to emit POST request, instead of GET. + """ + self.client.login(username='team', password='team') + + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertTemplateUsed(r, 'kfet/confirm_form.html') + + def test_notoken_post(self): + """ + POST request without token in COOKIES sets a token and redirects to + logout url. + """ + self.client.login(username='team', password='team') + + r = self.client.post(self.url) + + self.assertRedirects( + r, '/logout?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def test_notoken_not_team(self): + """ + Logged in user must be a team user to initiate login as generic user. + """ + self.client.login(username='user', password='user') + + # With GET. + r = self.client.get(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + # Also with POST. + r = self.client.post(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def _set_signed_cookie(self, client, key, value): + signed_value = signing.get_cookie_signer(salt=key).sign(value) + client.cookies.load({key: signed_value}) + + def _is_cookie_deleted(self, client, key): + try: + self.assertNotIn(key, client.cookies) + except AssertionError: + try: + cookie = client.cookies[key] + # It also can be emptied. + self.assertEqual(cookie.value, '') + self.assertEqual( + cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') + self.assertEqual(cookie['max-age'], 0) + except AssertionError: + raise AssertionError("The cookie '%s' still exists." % key) + + def test_withtoken_valid(self): + """ + The kfet generic user is logged in. + """ + token = GenericTeamToken.objects.create(token='valid') + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid') + + r = self.client.get(self.url) + + self.assertRedirects(r, reverse('kfet.kpsul')) + self.assertEqual(r.wsgi_request.user, self.generic_user) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + with self.assertRaises(GenericTeamToken.DoesNotExist): + token.refresh_from_db() + + def test_withtoken_invalid(self): + """ + If token is invalid, delete it and try again. + """ + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid') + + r = self.client.get(self.url) + + self.assertRedirects(r, self.url, fetch_redirect_response=False) + self.assertEqual(r.wsgi_request.user, AnonymousUser()) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + + def test_flow_ok(self): + """ + A team user is logged in as the kfet generic user. + """ + self.client.login(username='team', password='team') + next_url = '/k-fet/' + + r = self.client.post( + '{}?next={}'.format(self.url, next_url), follow=True) + + self.assertEqual(r.wsgi_request.user, self.generic_user) + self.assertEqual(r.wsgi_request.path, '/k-fet/') + + +## +# Temporary authentication +# +# Includes: +# - TemporaryAuthMiddleware +# - temporary_auth context processor +## + +class TemporaryAuthTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + self.factory = RequestFactory() + + user1_acc = Account(trigramme='000') + user1_acc.change_pwd('kfet_user1') + user1_acc.save({'username': 'user1'}) + self.user1 = user1_acc.user + self.user1.set_password('user1') + self.user1.save() + + user2_acc = Account(trigramme='100') + user2_acc.change_pwd('kfet_user2') + user2_acc.save({'username': 'user2'}) + self.user2 = user2_acc.user + self.user2.set_password('user2') + self.user2.save() + + self.perm = Permission.objects.get( + content_type__app_label='kfet', codename='is_team') + self.user2.user_permissions.add(self.perm) + + def test_middleware_header(self): + """ + A user can be authenticated if ``HTTP_KFETPASSWORD`` header of a + request contains a valid kfet password. + """ + request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_post(self): + """ + A user can be authenticated if ``KFETPASSWORD`` of POST data contains + a valid kfet password. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_invalid(self): + """ + The given password must be a password of an Account. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user1) + self.assertFalse(hasattr(request, 'real_user')) + + def test_context_processor(self): + """ + Context variables give the real authenticated user and his permissions. + """ + self.client.login(username='user1', password='user1') + + r = self.client.get('/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + + self.assertEqual(r.context['user'], self.user1) + self.assertNotIn('kfet.is_team', r.context['perms']) + + def test_auth_not_persistent(self): + """ + The authentication is temporary, i.e. for one request. + """ + self.client.login(username='user1', password='user1') + + r1 = self.client.get( + '/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + self.assertEqual(r1.wsgi_request.user, self.user2) + + r2 = self.client.get('/k-fet/accounts/') + self.assertEqual(r2.wsgi_request.user, self.user1) diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py new file mode 100644 index 00000000..0edc555d --- /dev/null +++ b/kfet/auth/utils.py @@ -0,0 +1,34 @@ +import hashlib + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission + +from kfet.models import Account + +User = get_user_model() + + +def get_kfet_generic_user(): + """ + Return the user related to the kfet generic account. + """ + return Account.objects.get_generic().user + + +def setup_kfet_generic_user(**kwargs): + """ + First steps of setup of the kfet generic user are done in a migration, as + it is more robust against database schema changes. + Following steps cannot be done from migration. + """ + generic = get_kfet_generic_user() + generic.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', + codename='is_team', + ) + ) + + +def hash_password(password): + return hashlib.sha256(password.encode('utf-8')).hexdigest() diff --git a/kfet/auth/views.py b/kfet/auth/views.py new file mode 100644 index 00000000..7b9f4099 --- /dev/null +++ b/kfet/auth/views.py @@ -0,0 +1,136 @@ +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib.auth import authenticate, login +from django.contrib.auth.decorators import permission_required +from django.contrib.auth.models import Group, User +from django.contrib.auth.views import redirect_to_login +from django.core.urlresolvers import reverse, reverse_lazy +from django.db.models import Prefetch +from django.http import QueryDict +from django.shortcuts import redirect, render +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import View +from django.views.decorators.http import require_http_methods +from django.views.generic.edit import CreateView, UpdateView + +from .forms import GroupForm +from .models import GenericTeamToken + + +class GenericLoginView(View): + """ + View to authenticate as kfet generic user. + + It is a 2-step view. First, issue a token if user is a team member and send + him to the logout view (for proper disconnect) with callback url to here. + Then authenticate the token to log in as the kfet generic user. + + Token is stored in COOKIES to avoid share it with the authentication + provider, which can be external. Session is unusable as it will be cleared + on logout. + """ + TOKEN_COOKIE_NAME = 'kfettoken' + + @method_decorator(require_http_methods(['GET', 'POST'])) + def dispatch(self, request, *args, **kwargs): + token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) + if not token: + if not request.user.has_perm('kfet.is_team'): + return redirect_to_login(request.get_full_path()) + + if request.method == 'POST': + # Step 1: set token and logout user. + return self.prepare_auth() + else: + # GET request should not change server/client states. Send a + # confirmation template to emit a POST request. + return render(request, 'kfet/confirm_form.html', { + 'title': _("Ouvrir une session partagée"), + 'text': _( + "Êtes-vous sûr·e de vouloir ouvrir une session " + "partagée ?" + ), + }) + else: + # Step 2: validate token. + return self.validate_auth(token) + + def prepare_auth(self): + # Issue token. + token = GenericTeamToken.objects.create_token() + + # Prepare callback of logout. + here_url = reverse(login_generic) + if 'next' in self.request.GET: + # Keep given next page. + here_qd = QueryDict(mutable=True) + here_qd['next'] = self.request.GET['next'] + here_url += '?{}'.format(here_qd.urlencode()) + + logout_url = reverse('cof-logout') + logout_qd = QueryDict(mutable=True) + logout_qd['next'] = here_url + logout_url += '?{}'.format(logout_qd.urlencode(safe='/')) + + resp = redirect(logout_url) + resp.set_signed_cookie( + self.TOKEN_COOKIE_NAME, token.token, httponly=True) + return resp + + def validate_auth(self, token): + # Authenticate with GenericBackend. + user = authenticate(request=self.request, kfet_token=token) + + if user: + # Log in generic user. + login(self.request, user) + messages.success(self.request, _( + "K-Fêt — Ouverture d'une session partagée." + )) + resp = redirect(self.get_next_url()) + else: + # Try again. + resp = redirect(self.request.get_full_path()) + + # Prevents blocking due to an invalid COOKIE. + resp.delete_cookie(self.TOKEN_COOKIE_NAME) + return resp + + def get_next_url(self): + return self.request.GET.get('next', reverse('kfet.kpsul')) + + +login_generic = GenericLoginView.as_view() + + +@permission_required('kfet.manage_perms') +def account_group(request): + user_pre = Prefetch( + 'user_set', + queryset=User.objects.select_related('profile__account_kfet'), + ) + groups = ( + Group.objects + .filter(name__icontains='K-Fêt') + .prefetch_related('permissions', user_pre) + ) + return render(request, 'kfet/account_group.html', { + 'groups': groups, + }) + + +class AccountGroupCreate(SuccessMessageMixin, CreateView): + model = Group + template_name = 'kfet/account_group_form.html' + form_class = GroupForm + success_message = 'Nouveau groupe : %(name)s' + success_url = reverse_lazy('kfet.account.group') + + +class AccountGroupUpdate(SuccessMessageMixin, UpdateView): + queryset = Group.objects.filter(name__icontains='K-Fêt') + template_name = 'kfet/account_group_form.html' + form_class = GroupForm + success_message = 'Groupe modifié : %(name)s' + success_url = reverse_lazy('kfet.account.group') diff --git a/kfet/backends.py b/kfet/backends.py deleted file mode 100644 index fb9538d0..00000000 --- a/kfet/backends.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -import hashlib - -from django.contrib.auth.models import User, Permission -from gestioncof.models import CofProfile -from kfet.models import Account, GenericTeamToken - - -class KFetBackend(object): - def authenticate(self, request): - password = request.POST.get('KFETPASSWORD', '') - password = request.META.get('HTTP_KFETPASSWORD', password) - if not password: - return None - - try: - password_sha256 = ( - hashlib.sha256(password.encode('utf-8')) - .hexdigest() - ) - account = Account.objects.get(password=password_sha256) - return account.cofprofile.user - except Account.DoesNotExist: - return None - - -class GenericTeamBackend(object): - def authenticate(self, username=None, token=None): - valid_token = GenericTeamToken.objects.get(token=token) - if username == 'kfet_genericteam' and valid_token: - # Création du user s'il n'existe pas déjà - user, _ = User.objects.get_or_create(username='kfet_genericteam') - profile, _ = CofProfile.objects.get_or_create(user=user) - account, _ = Account.objects.get_or_create( - cofprofile=profile, - trigramme='GNR') - - # Ajoute la permission kfet.is_team à ce user - perm_is_team = Permission.objects.get(codename='is_team') - user.user_permissions.add(perm_is_team) - - return user - return None - - def get_user(self, user_id): - try: - return ( - User.objects - .select_related('profile__account_kfet') - .get(pk=user_id) - ) - except User.DoesNotExist: - return None diff --git a/kfet/cms/fixtures/kfet_wagtail_17_05.json b/kfet/cms/fixtures/kfet_wagtail_17_05.json index f6a46c30..66ac7040 100644 --- a/kfet/cms/fixtures/kfet_wagtail_17_05.json +++ b/kfet/cms/fixtures/kfet_wagtail_17_05.json @@ -53,7 +53,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -83,7 +83,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -113,7 +113,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -143,7 +143,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -173,7 +173,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -203,7 +203,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -233,7 +233,7 @@ "page" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -263,7 +263,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -681,7 +681,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Plan d'acc\u00e8s", @@ -694,7 +694,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Demande d'autorisation", @@ -707,7 +707,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Trait\u00e9 de Flipper Th\u00e9orique", @@ -730,7 +730,7 @@ "title": "K-F\u00eat - Amazon Hunt", "width": 200, "uploaded_by_user": [ - "root" + "kfet_genericteam" ] } }, @@ -750,7 +750,7 @@ "title": "K-F\u00eat - Fun Machine", "width": 200, "uploaded_by_user": [ - "root" + "kfet_genericteam" ] } }, @@ -767,7 +767,7 @@ "title": "Hugo Manet", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -787,7 +787,7 @@ "title": "Lisa Gourdon", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -807,7 +807,7 @@ "title": "Pierre Quesselaire", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -827,7 +827,7 @@ "title": "Thibault Scoquard", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -847,7 +847,7 @@ "title": "Arnaud Fanthomme", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -867,7 +867,7 @@ "title": "Vincent Balerdi", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -887,7 +887,7 @@ "title": "Nathana\u00ebl Willaime", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -907,7 +907,7 @@ "title": "\u00c9lisabeth Miller", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -927,7 +927,7 @@ "title": "Arthur Lesage", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -947,7 +947,7 @@ "title": "Sarah Asset", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -967,7 +967,7 @@ "title": "Alexandre Legrand", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -987,7 +987,7 @@ "title": "\u00c9tienne Baudel", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1007,7 +1007,7 @@ "title": "Marine Snape", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1027,7 +1027,7 @@ "title": "Anatole Gosset", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1047,7 +1047,7 @@ "title": "Jacko Rastikian", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1067,7 +1067,7 @@ "title": "Alexandre Jannaud", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1087,7 +1087,7 @@ "title": "Aur\u00e9lien Delobelle", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1107,7 +1107,7 @@ "title": "Sylvain Douteau", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1127,7 +1127,7 @@ "title": "Rapha\u00ebl Lescanne", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1147,7 +1147,7 @@ "title": "Romain Gourvil", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1167,7 +1167,7 @@ "title": "Marie Labeye", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1187,7 +1187,7 @@ "title": "Oscar Blumberg", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1207,7 +1207,7 @@ "title": "Za\u00efd Allybokus", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1227,7 +1227,7 @@ "title": "Damien Garreau", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1247,7 +1247,7 @@ "title": "Andr\u00e9a Londono-Lopez", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1267,7 +1267,7 @@ "title": "Tristan Roussel", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1287,7 +1287,7 @@ "title": "Guillaume Vernade", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1307,7 +1307,7 @@ "title": "Lucas Mercier", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1327,7 +1327,7 @@ "title": "Fran\u00e7ois Maillot", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1347,7 +1347,7 @@ "title": "Fabrice Catoire", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 4c7b4fe4..04feec81 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -1,18 +1,7 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.context_processors import PermWrapper - from kfet.config import kfet_config -def auth(request): - if hasattr(request, 'real_user'): - return { - 'user': request.real_user, - 'perms': PermWrapper(request.real_user), - } - return {} - - def config(request): return {'kfet_config': kfet_config} diff --git a/kfet/forms.py b/kfet/forms.py index 7bafbdd7..963e4254 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -5,9 +5,8 @@ from decimal import Decimal from django import forms from django.core.exceptions import ValidationError -from django.contrib.auth.models import User, Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.forms import modelformset_factory, widgets +from django.contrib.auth.models import User +from django.forms import modelformset_factory from django.utils import timezone from djconfig.forms import ConfigForm @@ -18,6 +17,8 @@ from kfet.models import ( TransferGroup, Supplier) from gestioncof.models import CofProfile +from .auth.forms import UserGroupForm # noqa + # ----- # Widgets @@ -128,65 +129,6 @@ class UserRestrictTeamForm(UserForm): fields = ['first_name', 'last_name', 'email'] -class UserGroupForm(forms.ModelForm): - groups = forms.ModelMultipleChoiceField( - Group.objects.filter(name__icontains='K-Fêt'), - label='Statut équipe', - required=False) - - def clean_groups(self): - kfet_groups = self.cleaned_data.get('groups') - if self.instance.pk is None: - return kfet_groups - other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') - return list(kfet_groups) + list(other_groups) - - class Meta: - model = User - fields = ['groups'] - - -class KFetPermissionsField(forms.ModelMultipleChoiceField): - - def __init__(self, *args, **kwargs): - queryset = Permission.objects.filter( - content_type__in=ContentType.objects.filter(app_label="kfet"), - ) - super().__init__( - queryset=queryset, - widget=widgets.CheckboxSelectMultiple, - *args, **kwargs - ) - - def label_from_instance(self, obj): - return obj.name - - -class GroupForm(forms.ModelForm): - permissions = KFetPermissionsField() - - def clean_name(self): - name = self.cleaned_data['name'] - return 'K-Fêt %s' % name - - def clean_permissions(self): - kfet_perms = self.cleaned_data['permissions'] - # TODO: With Django >=1.11, the QuerySet method 'difference' can be used. - # other_groups = self.instance.permissions.difference( - # self.fields['permissions'].queryset - # ) - if self.instance.pk is None: - return kfet_perms - other_perms = self.instance.permissions.exclude( - pk__in=[p.pk for p in self.fields['permissions'].queryset], - ) - return list(kfet_perms) + list(other_perms) - - class Meta: - model = Group - fields = ['name', 'permissions'] - - class AccountNegativeForm(forms.ModelForm): class Meta: model = AccountNegative diff --git a/kfet/middleware.py b/kfet/middleware.py deleted file mode 100644 index 9502d393..00000000 --- a/kfet/middleware.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.contrib.auth.models import User - -from kfet.backends import KFetBackend - - -class KFetAuthenticationMiddleware(object): - """Authenticate another user for this request if KFetBackend succeeds. - - By the way, if a user is authenticated, we refresh its from db to add - values from CofProfile and Account of this user. - - """ - def process_request(self, request): - if request.user.is_authenticated(): - # avoid multiple db accesses in views and templates - user_pk = request.user.pk - request.user = ( - User.objects - .select_related('profile__account_kfet') - .get(pk=user_pk) - ) - - kfet_backend = KFetBackend() - temp_request_user = kfet_backend.authenticate(request) - if temp_request_user: - request.real_user = request.user - request.user = temp_request_user diff --git a/kfet/migrations/0058_delete_genericteamtoken.py b/kfet/migrations/0058_delete_genericteamtoken.py new file mode 100644 index 00000000..ea8b55cd --- /dev/null +++ b/kfet/migrations/0058_delete_genericteamtoken.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0057_merge'), + ] + + operations = [ + migrations.DeleteModel( + name='GenericTeamToken', + ), + ] diff --git a/kfet/migrations/0059_create_generic.py b/kfet/migrations/0059_create_generic.py new file mode 100644 index 00000000..4f04770c --- /dev/null +++ b/kfet/migrations/0059_create_generic.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + +from kfet.auth import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME + + +def setup_kfet_generic_user(apps, schema_editor): + """ + Setup models instances for the kfet generic account. + + Username and trigramme are retrieved from kfet.auth.__init__ module. + Other data are registered here. + + See also setup_kfet_generic_user from kfet.auth.utils module. + """ + User = apps.get_model('auth', 'User') + CofProfile = apps.get_model('gestioncof', 'CofProfile') + Account = apps.get_model('kfet', 'Account') + + user, _ = User.objects.update_or_create( + username=KFET_GENERIC_USERNAME, + defaults={ + 'first_name': 'Compte générique K-Fêt', + }, + ) + profile, _ = CofProfile.objects.update_or_create(user=user) + account, _ = Account.objects.update_or_create( + cofprofile=profile, + defaults={ + 'trigramme': KFET_GENERIC_TRIGRAMME, + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0058_delete_genericteamtoken'), + ] + + operations = [ + migrations.RunPython(setup_kfet_generic_user), + ] diff --git a/kfet/models.py b/kfet/models.py index fb0d8813..b1e351d5 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -13,7 +13,9 @@ from django.db import transaction from django.db.models import F from datetime import date import re -import hashlib + +from .auth import KFET_GENERIC_TRIGRAMME +from .auth.models import GenericTeamToken # noqa from .config import kfet_config from .utils import to_ukf @@ -34,6 +36,23 @@ class AccountManager(models.Manager): return super().get_queryset().select_related('cofprofile__user', 'negative') + def get_generic(self): + """ + Get the kfet generic account instance. + """ + return self.get(trigramme=KFET_GENERIC_TRIGRAMME) + + def get_by_password(self, password): + """ + Get a kfet generic account by clear password. + + Raises Account.DoesNotExist if no Account has this password. + """ + from .auth.utils import hash_password + if password is None: + raise self.model.DoesNotExist + return self.get(password=hash_password(password)) + class Account(models.Model): objects = AccountManager() @@ -239,10 +258,9 @@ class Account(models.Model): self.cofprofile = cof super(Account, self).save(*args, **kwargs) - def change_pwd(self, pwd): - pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\ - .hexdigest() - self.password = pwd_sha256 + def change_pwd(self, clear_password): + from .auth.utils import hash_password + self.password = hash_password(clear_password) # Surcharge de delete # Pas de suppression possible @@ -716,7 +734,3 @@ class Operation(models.Model): return templates[self.type].format(nb=self.article_nb, article=self.article, amount=self.amount) - - -class GenericTeamToken(models.Model): - token = models.CharField(max_length = 50, unique = True) diff --git a/kfet/signals.py b/kfet/signals.py deleted file mode 100644 index c677ac9c..00000000 --- a/kfet/signals.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.contrib import messages -from django.contrib.auth.signals import user_logged_in -from django.core.urlresolvers import reverse -from django.dispatch import receiver -from django.utils.safestring import mark_safe - - -@receiver(user_logged_in) -def messages_on_login(sender, request, user, **kwargs): - if (not user.username == 'kfet_genericteam' and - user.has_perm('kfet.is_team') and - hasattr(request, 'GET') and - 'k-fet' in request.GET.get('next', '')): - messages.info(request, mark_safe( - '' - ' Connexion en utilisateur partagé ?' - '' - .format(reverse('kfet.login.genericteam')) - )) diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index b34f2005..75b80b04 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,22 +1,33 @@ -$(document).ready(function() { - if (typeof Cookies !== 'undefined') { - // Retrieving csrf token - csrftoken = Cookies.get('csrftoken'); - // Appending csrf token to ajax post requests - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +/** + * CSRF Token + */ + +var csrftoken = ''; +if (typeof Cookies !== 'undefined') + csrftoken = Cookies.get('csrftoken'); + +// Add CSRF token in header of AJAX requests. + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); } - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - } - }); } }); +function add_csrf_form($form) { + $form.append( + $('', {'name': 'csrfmiddlewaretoken', 'value': csrftoken}) + ); +} + + /* * Generic Websocket class and k-psul ws instanciation */ @@ -199,3 +210,28 @@ jconfirm.defaults = { confirmButton: '', cancelButton: '' }; + + +/** + * Create form node, given an url used as 'action', with csrftoken set. + */ +function create_form(url) { + let $form = $('
', { + 'action': url, + 'method': 'post', + }); + add_csrf_form($form); + return $form; +} + + +/** + * Emit a POST request from tag. + * + * Usage: + * {…} + */ +function submit_url(el) { + let url = $(el).data('url'); + create_form(url).appendTo($('body')).submit(); +} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index abcb8e18..dda6c1ef 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,4 +1,4 @@ -{% load static %} +{% load i18n static %} {% load wagtailcore_tags %} - - diff --git a/kfet/templates/kfet/confirm_form.html b/kfet/templates/kfet/confirm_form.html new file mode 100644 index 00000000..1cffd171 --- /dev/null +++ b/kfet/templates/kfet/confirm_form.html @@ -0,0 +1,20 @@ +{% extends "kfet/base_form.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block header %}{% endblock %} + +{% block main-class %}main-bg main-padding text-center{% endblock %} +{% block main-size %}col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3{% endblock %} + +{% block main %} + + +

+ {{ text }} +

+ + {% csrf_token %} +
+ +{% endblock %} diff --git a/kfet/templates/kfet/login_genericteam.html b/kfet/templates/kfet/login_genericteam.html deleted file mode 100644 index d2f8eca0..00000000 --- a/kfet/templates/kfet/login_genericteam.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'kfet/base.html' %} - -{% block extra_head %} - -{% endblock %} diff --git a/kfet/templates/kfet/nav_item.html b/kfet/templates/kfet/nav_item.html index 8bc311b8..b5981266 100644 --- a/kfet/templates/kfet/nav_item.html +++ b/kfet/templates/kfet/nav_item.html @@ -4,6 +4,8 @@
  • {% if href %} + {% else %} + {% endif %} {% if href %} + {% else %} + {% endif %}
  • diff --git a/kfet/tests/test_forms.py b/kfet/tests/test_forms.py deleted file mode 100644 index 7f129a3f..00000000 --- a/kfet/tests/test_forms.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.test import TestCase -from django.contrib.auth.models import User, Group - -from kfet.forms import UserGroupForm - - -class UserGroupFormTests(TestCase): - """Test suite for UserGroupForm.""" - - def setUp(self): - # create user - self.user = User.objects.create(username="foo", password="foo") - - # create some K-Fêt groups - prefix_name = "K-Fêt " - names = ["Group 1", "Group 2", "Group 3"] - self.kfet_groups = [ - Group.objects.create(name=prefix_name+name) - for name in names - ] - - # create a non-K-Fêt group - self.other_group = Group.objects.create(name="Other group") - - def test_choices(self): - """Only K-Fêt groups are selectable.""" - form = UserGroupForm(instance=self.user) - groups_field = form.fields['groups'] - self.assertQuerysetEqual( - groups_field.queryset, - [repr(g) for g in self.kfet_groups], - ordered=False, - ) - - def test_keep_others(self): - """User stays in its non-K-Fêt groups.""" - user = self.user - - # add user to a non-K-Fêt group - user.groups.add(self.other_group) - - # add user to some K-Fêt groups through UserGroupForm - data = { - 'groups': [group.pk for group in self.kfet_groups], - } - form = UserGroupForm(data, instance=user) - - form.is_valid() - form.save() - self.assertQuerysetEqual( - user.groups.all(), - [repr(g) for g in [self.other_group] + self.kfet_groups], - ordered=False, - ) diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py new file mode 100644 index 00000000..ea132acd --- /dev/null +++ b/kfet/tests/test_models.py @@ -0,0 +1,25 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from kfet.models import Account + +User = get_user_model() + + +class AccountTests(TestCase): + + def setUp(self): + self.account = Account(trigramme='000') + self.account.save({'username': 'user'}) + + def test_password(self): + self.account.change_pwd('anna') + self.account.save() + + self.assertEqual(Account.objects.get_by_password('anna'), self.account) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password(None) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password('bernard') diff --git a/kfet/urls.py b/kfet/urls.py index 17ded7b8..f39299a5 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -8,8 +8,8 @@ from kfet.decorators import teamkfet_required urlpatterns = [ - url(r'^login/genericteam$', views.login_genericteam, - name='kfet.login.genericteam'), + url(r'^login/generic$', views.login_generic, + name='kfet.login.generic'), url(r'^history$', views.history, name='kfet.history'), diff --git a/kfet/views.py b/kfet/views.py index 5021daab..7a28819e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -12,34 +12,30 @@ from django.views.generic.edit import CreateView, UpdateView from django.core.urlresolvers import reverse, reverse_lazy from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth import authenticate, login -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User, Permission, Group +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User, Permission from django.http import JsonResponse, Http404 from django.forms import formset_factory from django.db import transaction from django.db.models import F, Sum, Prefetch, Count from django.db.models.functions import Coalesce from django.utils import timezone -from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator -from django_cas_ng.views import logout as cas_logout_view - from gestioncof.models import CofProfile from kfet.config import kfet_config from kfet.decorators import teamkfet_required from kfet.models import ( Account, Checkout, Article, AccountNegative, - CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, + CheckoutStatement, Supplier, SupplierArticle, Inventory, InventoryArticle, Order, OrderArticle, Operation, OperationGroup, TransferGroup, Transfer, ArticleCategory) from kfet.forms import ( AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm, UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm, AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm, - GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, + CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm, KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, KPsulOperationFormSet, AddcostForm, FilterHistoryForm, @@ -54,25 +50,9 @@ import heapq import statistics from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale - -@teamkfet_required -def login_genericteam(request): - # Check si besoin de déconnecter l'utilisateur de CAS - cas_logout = None - if request.user.profile.login_clipper: - # Récupèration de la vue de déconnexion de CAS - # Ici, car request sera modifié après - next_page = request.META.get('HTTP_REFERER', None) - cas_logout = cas_logout_view(request, next_page=next_page) - - # Authentification du compte générique - token = GenericTeamToken.objects.create(token=get_random_string(50)) - user = authenticate(username="kfet_genericteam", token=token.token) - login(request, user) - - messages.success(request, "Connecté en utilisateur partagé") - - return cas_logout or render(request, "kfet/login_genericteam.html") +from .auth.views import ( # noqa + account_group, login_generic, AccountGroupCreate, AccountGroupUpdate, +) def put_cleaned_data_in_dict(dict, form): @@ -505,37 +485,6 @@ def account_update(request, trigramme): }) -@permission_required('kfet.manage_perms') -def account_group(request): - user_pre = Prefetch( - 'user_set', - queryset=User.objects.select_related('profile__account_kfet'), - ) - groups = ( - Group.objects - .filter(name__icontains='K-Fêt') - .prefetch_related('permissions', user_pre) - ) - return render(request, 'kfet/account_group.html', { - 'groups': groups, - }) - - -class AccountGroupCreate(SuccessMessageMixin, CreateView): - model = Group - template_name = 'kfet/account_group_form.html' - form_class = GroupForm - success_message = 'Nouveau groupe : %(name)s' - success_url = reverse_lazy('kfet.account.group') - -class AccountGroupUpdate(SuccessMessageMixin, UpdateView): - queryset = Group.objects.filter(name__icontains='K-Fêt') - template_name = 'kfet/account_group_form.html' - form_class = GroupForm - success_message = 'Groupe modifié : %(name)s' - success_url = reverse_lazy('kfet.account.group') - - class AccountNegativeList(ListView): queryset = ( AccountNegative.objects