Merge branch 'Aufinal/fix_privilege_escalation' into 'master'

Fix le problème d'auth par mdp K-Fêt

Closes #240

See merge request klub-dev-ens/gestioCOF!380
This commit is contained in:
Martin Pepin 2019-11-22 14:37:13 +01:00
commit 82746f1492
7 changed files with 45 additions and 86 deletions

View file

@ -111,7 +111,6 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"kfet.auth.middleware.TemporaryAuthMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",

23
kfet/auth/decorators.py Normal file
View file

@ -0,0 +1,23 @@
from functools import wraps
from kfet.auth.backends import AccountBackend
def kfet_password_auth(view_func):
def get_kfet_password(request):
return request.META.get("HTTP_KFETPASSWORD") or request.POST.get("KFETPASSWORD")
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if request.method == "POST":
temp_request_user = AccountBackend().authenticate(
request, kfet_password=get_kfet_password(request)
)
if temp_request_user:
request.real_user = request.user
request.user = temp_request_user
return view_func(request, *args, **kwargs)
return _wrapped_view

View file

@ -1,37 +0,0 @@
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 __init__(self, get_response):
self.get_response = get_response
def __call__(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
return self.get_response(request)
def get_kfet_password(self, request):
return request.META.get("HTTP_KFETPASSWORD") or request.POST.get("KFETPASSWORD")

View file

@ -10,7 +10,6 @@ from kfet.models import Account
from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME
from .backends import AccountBackend, GenericBackend from .backends import AccountBackend, GenericBackend
from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken from .models import GenericTeamToken
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user
from .views import GenericLoginView from .views import GenericLoginView
@ -268,8 +267,6 @@ class TemporaryAuthTests(TestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
self.middleware = TemporaryAuthMiddleware(mock.Mock())
user1_acc = Account(trigramme="000") user1_acc = Account(trigramme="000")
user1_acc.change_pwd("kfet_user1") user1_acc.change_pwd("kfet_user1")
user1_acc.save({"username": "user1"}) user1_acc.save({"username": "user1"})
@ -289,51 +286,13 @@ class TemporaryAuthTests(TestCase):
) )
self.user2.user_permissions.add(self.perm) 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
self.middleware(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
self.middleware(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
self.middleware(request)
self.assertEqual(request.user, self.user1)
self.assertFalse(hasattr(request, "real_user"))
def test_context_processor(self): def test_context_processor(self):
""" """
Context variables give the real authenticated user and his permissions. Context variables give the real authenticated user and his permissions.
""" """
self.client.login(username="user1", password="user1") self.client.login(username="user1", password="user1")
r = self.client.get("/k-fet/accounts/", HTTP_KFETPASSWORD="kfet_user2") r = self.client.post("/k-fet/accounts/000/edit", HTTP_KFETPASSWORD="kfet_user2")
self.assertEqual(r.context["user"], self.user1) self.assertEqual(r.context["user"], self.user1)
self.assertNotIn("kfet.is_team", r.context["perms"]) self.assertNotIn("kfet.is_team", r.context["perms"])
@ -344,8 +303,10 @@ class TemporaryAuthTests(TestCase):
""" """
self.client.login(username="user1", password="user1") self.client.login(username="user1", password="user1")
r1 = self.client.get("/k-fet/accounts/", HTTP_KFETPASSWORD="kfet_user2") r1 = self.client.post(
"/k-fet/accounts/000/edit", HTTP_KFETPASSWORD="kfet_user2"
)
self.assertEqual(r1.wsgi_request.user, self.user2) self.assertEqual(r1.wsgi_request.user, self.user2)
r2 = self.client.get("/k-fet/accounts/") r2 = self.client.post("/k-fet/accounts/000/edit")
self.assertEqual(r2.wsgi_request.user, self.user1) self.assertEqual(r2.wsgi_request.user, self.user1)

View file

@ -30,9 +30,6 @@
</div> </div>
</div> </div>
{% include "kfet/form_field_snippet.html" with field=form.permissions %} {% include "kfet/form_field_snippet.html" with field=form.permissions %}
{% if not perms.kfet.manage_perms %}
{% include "kfet/form_authentication_snippet.html" %}
{% endif %}
{% include "kfet/form_submit_snippet.html" with value="Enregistrer" %} {% include "kfet/form_submit_snippet.html" with value="Enregistrer" %}
</form> </form>

View file

@ -24,6 +24,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet import KFET_DELETED_TRIGRAMME, consumers from kfet import KFET_DELETED_TRIGRAMME, consumers
from kfet.auth.decorators import kfet_password_auth
from kfet.config import kfet_config from kfet.config import kfet_config
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.forms import ( from kfet.forms import (
@ -119,6 +120,7 @@ def account_is_validandfree_ajax(request):
@login_required @login_required
@teamkfet_required @teamkfet_required
@kfet_password_auth
def account_create(request): def account_create(request):
# Enregistrement # Enregistrement
@ -320,6 +322,7 @@ def account_read(request, trigramme):
@login_required @login_required
@kfet_password_auth
def account_update(request, trigramme): def account_update(request, trigramme):
account = get_object_or_404(Account, trigramme=trigramme) account = get_object_or_404(Account, trigramme=trigramme)
@ -518,6 +521,7 @@ class CheckoutList(ListView):
# Checkout - Create # Checkout - Create
@method_decorator(kfet_password_auth, name="dispatch")
class CheckoutCreate(SuccessMessageMixin, CreateView): class CheckoutCreate(SuccessMessageMixin, CreateView):
model = Checkout model = Checkout
template_name = "kfet/checkout_create.html" template_name = "kfet/checkout_create.html"
@ -629,6 +633,7 @@ def getAmountBalance(data):
) )
@method_decorator(kfet_password_auth, name="dispatch")
class CheckoutStatementCreate(SuccessMessageMixin, CreateView): class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
model = CheckoutStatement model = CheckoutStatement
template_name = "kfet/checkoutstatement_create.html" template_name = "kfet/checkoutstatement_create.html"
@ -665,6 +670,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
@method_decorator(kfet_password_auth, name="dispatch")
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
model = CheckoutStatement model = CheckoutStatement
template_name = "kfet/checkoutstatement_update.html" template_name = "kfet/checkoutstatement_update.html"
@ -705,6 +711,7 @@ class CategoryList(ListView):
# Category - Update # Category - Update
@method_decorator(kfet_password_auth, name="dispatch")
class CategoryUpdate(SuccessMessageMixin, UpdateView): class CategoryUpdate(SuccessMessageMixin, UpdateView):
model = ArticleCategory model = ArticleCategory
template_name = "kfet/category_update.html" template_name = "kfet/category_update.html"
@ -959,6 +966,7 @@ def kpsul_checkout_data(request):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def kpsul_update_addcost(request): def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST) addcost_form = AddcostForm(request.POST)
@ -996,6 +1004,7 @@ def get_missing_perms(required_perms, user):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def kpsul_perform_operations(request): def kpsul_perform_operations(request):
# Initializing response data # Initializing response data
data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}}
@ -1187,6 +1196,7 @@ def kpsul_perform_operations(request):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def kpsul_cancel_operations(request): def kpsul_cancel_operations(request):
# Pour la réponse # Pour la réponse
data = {"canceled": [], "warnings": {}, "errors": {}} data = {"canceled": [], "warnings": {}, "errors": {}}
@ -1545,6 +1555,7 @@ def transfers_create(request):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def perform_transfers(request): def perform_transfers(request):
data = {"errors": {}, "transfers": [], "transfergroup": 0} data = {"errors": {}, "transfers": [], "transfergroup": 0}
@ -1626,6 +1637,7 @@ def perform_transfers(request):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def cancel_transfers(request): def cancel_transfers(request):
# Pour la réponse # Pour la réponse
data = {"canceled": [], "warnings": {}, "errors": {}} data = {"canceled": [], "warnings": {}, "errors": {}}
@ -1739,6 +1751,7 @@ class InventoryList(ListView):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def inventory_create(request): def inventory_create(request):
articles = Article.objects.select_related("category").order_by( articles = Article.objects.select_related("category").order_by(
@ -1833,6 +1846,7 @@ class OrderList(ListView):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def order_create(request, pk): def order_create(request, pk):
supplier = get_object_or_404(Supplier, pk=pk) supplier = get_object_or_404(Supplier, pk=pk)
@ -1985,6 +1999,7 @@ class OrderRead(DetailView):
@teamkfet_required @teamkfet_required
@kfet_password_auth
def order_to_inventory(request, pk): def order_to_inventory(request, pk):
order = get_object_or_404(Order, pk=pk) order = get_object_or_404(Order, pk=pk)
@ -2092,6 +2107,7 @@ def order_to_inventory(request, pk):
) )
@method_decorator(kfet_password_auth, name="dispatch")
class SupplierUpdate(SuccessMessageMixin, UpdateView): class SupplierUpdate(SuccessMessageMixin, UpdateView):
model = Supplier model = Supplier
template_name = "kfet/supplier_form.html" template_name = "kfet/supplier_form.html"

View file

@ -19,6 +19,6 @@ ldap3
channels==1.1.5 channels==1.1.5
python-dateutil python-dateutil
wagtail==2.4.* wagtail==2.4.*
wagtailmenus==2.12.* wagtailmenus<3
wagtail-modeltranslation==0.10.* wagtail-modeltranslation==0.10.*
django-cors-headers==2.2.0 django-cors-headers==2.2.0