WIP: public: change permission denied handling (#8122) #218

Closed
yweber wants to merge 1 commits from wip/8122-private-page-custom-message into main
3 changed files with 100 additions and 7 deletions

View File

@ -0,0 +1,23 @@
{% extends "combo/page_template.html" %}
{% load i18n %}
{% block combo-content %}
<div>
<h2>{% trans "You do not have access to this page" %}</h2>
{% url 'auth_login' as login_url %}
{% if request.user.is_authenticated %}
<p>
{% trans "Your user do not have the permission to see this page. You can try to" %}
<a href="{% url 'auth_logout' %}?next={{ login_url | urlencode }}{{ '?next=' | urlencode }}{{ request.build_absolute_uri | urlencode }}">
{% trans "authenticate yourself using another account" %}
</a>.
</p>
{% else %}
Review

On ne devrait jamais tomber dans ce "else", c'est utile de le laisser ?

On ne devrait jamais tomber dans ce "else", c'est utile de le laisser ?
<p>
{% trans "This page is not publicly accessible. You can try to" %}
<a href="{{ login_url }}?next={{ request.build_absolute_uri | urlencode }}">
{% trans "authenticate yourself" %}</a>.
</p>
{% endif %}
</div>
{% endblock %}

View File

@ -24,6 +24,7 @@ from django.contrib import messages
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views
from django.contrib.auth.models import User
from django.contrib.auth.views import redirect_to_login
from django.core import signing
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
@ -545,10 +546,31 @@ def publish_page(request, page, status=200, template_name=None):
if not page.is_visible(request.user):
if not request.user.is_authenticated:
from django.contrib.auth.views import redirect_to_login
if Page.objects.exists() and all(
not p.is_visible(request.user) for p in Page.objects.filter(parent_id__isnull=True)
):
# if user is not connected and none of the first-level pages can
yweber marked this conversation as resolved Outdated

ici je mettrais un RequestContext plutôt qu'un Context, sinon tu risques d'avoir plus tard un ticket qui dit qu'on n'a pas accès à la variable site_base, par exemple

ici je mettrais un RequestContext plutôt qu'un Context, sinon tu risques d'avoir plus tard un ticket qui dit qu'on n'a pas accès à la variable site_base, par exemple
# be viewed, display generic django Permission Denied error
raise PermissionDenied()
return redirect_to_login(request.build_absolute_uri())
Review

Si j'arrive à suivre correctement, ça ne me semble pas approprié, ça voudrait dire qu'un premier accès sur le portail agent afficherait une page d'erreur, plutôt qu'envoyer l'usager se connecter.

J'ai l'impression qu'en fait tout est confus entre les différentes directions, la description un peu courte du ticket, le ticket lié toulouse qui demande une page 403 différente, mes commentaires, etc.

Il faut peut-être prendre du recul et refaire une description fonctionnelle un peu exhaustive de ce qu'on veut atteindre, avant de continuer sur le code.

Si j'arrive à suivre correctement, ça ne me semble pas approprié, ça voudrait dire qu'un premier accès sur le portail agent afficherait une page d'erreur, plutôt qu'envoyer l'usager se connecter. J'ai l'impression qu'en fait tout est confus entre les différentes directions, la description un peu courte du ticket, le ticket lié toulouse qui demande une page 403 différente, mes commentaires, etc. Il faut peut-être prendre du recul et refaire une description fonctionnelle un peu exhaustive de ce qu'on veut atteindre, avant de continuer sur le code.
Review

Si j'arrive à suivre correctement, ça ne me semble pas approprié, ça voudrait dire qu'un premier accès sur le portail agent afficherait une page d'erreur, plutôt qu'envoyer l'usager se connecter.

Dans le cas ou aucune page n'est accessible oui, j'avais (mal) compris que c'était ce qui était attendu ;)

J'ai l'impression qu'en fait tout est confus entre les différentes directions, la description un peu courte du ticket, le ticket lié toulouse qui demande une page 403 différente, mes commentaires, etc.
Il faut peut-être prendre du recul et refaire une description fonctionnelle un peu exhaustive de ce qu'on veut atteindre, avant de continuer sur le code.

Je pense qu'on peut fermer cette PR en attendant une description fonctionnelle plus détaillée de ce qui est attendu/voulu ?

> Si j'arrive à suivre correctement, ça ne me semble pas approprié, ça voudrait dire qu'un premier accès sur le portail agent afficherait une page d'erreur, plutôt qu'envoyer l'usager se connecter. Dans le cas ou aucune page n'est accessible oui, j'avais (mal) compris que c'était ce qui était attendu ;) > J'ai l'impression qu'en fait tout est confus entre les différentes directions, la description un peu courte du ticket, le ticket lié toulouse qui demande une page 403 différente, mes commentaires, etc. > Il faut peut-être prendre du recul et refaire une description fonctionnelle un peu exhaustive de ce qu'on veut atteindre, avant de continuer sur le code. Je pense qu'on peut fermer cette PR en attendant une description fonctionnelle plus détaillée de ce qui est attendu/voulu ?
raise PermissionDenied()
try:
yweber marked this conversation as resolved Outdated

Je ne sais pas si c'est une bonne idée de faire que la fausse page essaye d'utiliser le même thème que la page non-accessible ?

Je ne sais pas si c'est une bonne idée de faire que la fausse page essaye d'utiliser le même thème que la page non-accessible ?

Je ne suis pas sûr, ici on touche à mes limites de compréhension de combo :) je pense que tu peux mettre template_name='combo/private-page-error.html', l'hypothèse étant que c'est sûrement plus safe de ne pas mixer deux template différents

Je ne suis pas sûr, ici on touche à mes limites de compréhension de combo :) je pense que tu peux mettre `template_name='combo/private-page-error.html'`, l'hypothèse étant que c'est sûrement plus safe de ne pas mixer deux template différents

Je serais à dire de regarder ce qui est fait dans error404 et de faire pareil.

Je serais à dire de regarder ce qui est fait dans error404 et de faire pareil.

Merci à vous deux !

Ok, je force l'utilisation du template 'standard'

Merci à vous deux ! Ok, je force l'utilisation du template 'standard'

Ok, je force l'utilisation du template 'standard'

Ça n'est pas tout ce que error404 fait; il y a 1/ vérifier qu'il y a des pages du site accessible et si ça n'est pas le cas retourner un gabarit minimal, pour les mêmes raisons que sur la page 404 (c'est suite à un audit de sécurité, cf #41514); puis 2/ prendre une page qui aurait comme slug "403" et si elle n'existe pas, partir sur la base de qui est à l'accueil (cf #43851, c'est pour avoir le pied de page).

> Ok, je force l'utilisation du template 'standard' Ça n'est pas tout ce que error404 fait; il y a 1/ vérifier qu'il y a des pages du site accessible et si ça n'est pas le cas retourner un gabarit minimal, pour les mêmes raisons que sur la page 404 (c'est suite à un audit de sécurité, cf #41514); puis 2/ prendre une page qui aurait comme slug "403" et si elle n'existe pas, partir sur la base de qui est à l'accueil (cf #43851, c'est pour avoir le pied de page).

J'ai l'impression qu'on comprend le ticket de deux manières différentes :

  • j'avais compris que le but du ticket était d'avoir la possibilité d'ajouter un message (par page) à afficher si la page n'est pas accessible
  • je crois comprendre que tu me guide vers le fait d'implémenter une page d'erreur PermissionDenied configurable pour tout le portail (le message personnalisable serait une page avec "403" comme slug)

C'est bien ça ?

J'ai l'impression qu'on comprend le ticket de deux manières différentes : - j'avais compris que le but du ticket était d'avoir la possibilité d'ajouter un message (par page) à afficher si la page n'est pas accessible - je crois comprendre que tu me guide vers le fait d'implémenter une page d'erreur PermissionDenied configurable pour tout le portail (le message personnalisable serait une page avec "403" comme slug) C'est bien ça ?

Il me semble en effet que la direction prise répète des erreurs et questions déjà traitées pour la page 404; le point 1/ est important pour ne pas prochain audit se taper le même commentaire, le point 2/ relève davantage de l'anticipation des besoins, ne pas devoir défaire/refaire quand quelqu'un viendra souhaiter du gras ou un lien dans le texte personnalisé.

Il me semble en effet que la direction prise répète des erreurs et questions déjà traitées pour la page 404; le point 1/ est important pour ne pas prochain audit se taper le même commentaire, le point 2/ relève davantage de l'anticipation des besoins, ne pas devoir défaire/refaire quand quelqu'un viendra souhaiter du gras ou un lien dans le texte personnalisé.

Alors oui je m'éloigne ainsi du ticket, mais il ne donne pas de cas d'usage qui justifierait que le message soit différent par page et Benjamin y a lié le ticket #84346 qui correspond davantage à ma description.

Alors oui je m'éloigne ainsi du ticket, mais il ne donne pas de cas d'usage qui justifierait que le message soit différent par page et Benjamin y a lié le ticket #84346 qui correspond davantage à ma description.

Super, merci :) Je pars sur cette implémentation 404 like.

Super, merci :) Je pars sur cette implémentation 404 like.
page = Page.objects.get(slug='403')
template_name = None
if not page.is_visible(request.user):
raise Page.DoesNotExist
except Page.DoesNotExist:
if not request.user.is_authenticated:
# No custom 403 page, user not logged in : redirect to login
return redirect_to_login(request.build_absolute_uri())
# User is logged in but do not have suffisent permissions
yweber marked this conversation as resolved Outdated

les params de la chaîne à traduire sont à mettre à l'extérieur du _()

les params de la chaîne à traduire sont à mettre à l'extérieur du _()
# displaying a small template inviting to logout/login
page = Page.objects.filter(slug='index', parent=None).first() or Page()
page.redirect_url = None
page.public = True
page.title = _('Permission denied')
page.template_name = 'standard'
template_name = 'combo/403.html'
return publish_page(request, page, status=403, template_name=template_name)
if page.redirect_url:
yweber marked this conversation as resolved Outdated

La logique du code est bien mais je suggère de l'écrire tout autrement, en passant par un template dédié :

  • Après rendu du message d'erreur, le code ici devient juste return render(request, 'combo/private-page-error.html', context={'error_message': error_msg}, status=403)
  • Dans le template, gérer le cas non connecté et afficher le lien de connexion grâce un bloc {% if (ça devrait rendre mieux que de construire du html dans la vue)
    • À propos, la construction de l'URL de login peut se permettre d'être méga triviale, genre href="{% url "auth_login" %}?next={{ request.build_absolute_uri }}" (le gain en lisibilité outrepasse large le gain en robustesse de passer par urlparse, et le "next" est déjà hardcodé à plein endroits, pas besoin d'aller le chercher dans REDIRECT_FIELD_NAME)

N'hésite pas à me dire si tu trouves cette idée mauvaise ou qu'il y a une complexité que j'ai raté 😄

La logique du code est bien mais je suggère de l'écrire tout autrement, en passant par un template dédié : * Après rendu du message d'erreur, le code ici devient juste `return render(request, 'combo/private-page-error.html', context={'error_message': error_msg}, status=403)` * Dans le template, gérer le cas non connecté et afficher le lien de connexion grâce un bloc `{% if` (ça devrait rendre mieux que de construire du html dans la vue) * À propos, la construction de l'URL de login peut se permettre d'être méga triviale, genre `href="{% url "auth_login" %}?next={{ request.build_absolute_uri }}"` (le gain en lisibilité outrepasse large le gain en robustesse de passer par urlparse, et le "next" est déjà hardcodé à plein endroits, pas besoin d'aller le chercher dans REDIRECT_FIELD_NAME) N'hésite pas à me dire si tu trouves cette idée mauvaise ou qu'il y a une complexité que j'ai raté 😄
context = {'request': request}

View File

@ -337,10 +337,20 @@ def test_page_templated_redirect(app):
def test_page_private_unlogged(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard', public=False)
page.save()
resp = app.get('/', status=302)
assert resp.location.endswith('/login/?next=http%3A//testserver/')
Page.objects.create(title='Home', slug='index', template_name='standard', public=True)
Page.objects.create(title='private', slug='priv', template_name='standard', public=False)
resp = app.get('/priv/', status=302)
assert resp.location.endswith('/login/?next=http%3A//testserver/priv/')
# custom error page : returns 403
error_page = Page.objects.create(title='demo', slug='403', public=True)
resp = app.get('/priv/', status=403)
# custom error page not visible : redirect to login
yweber marked this conversation as resolved Outdated

Ça vaut le coup de taper un resp.click sur le lien de login, et vite fait vérifier qu'on tombe bien sur la page de connexion

Ça vaut le coup de taper un resp.click sur le lien de login, et vite fait vérifier qu'on tombe bien sur la page de connexion
error_page.public = False
error_page.save()
resp = app.get('/priv/', status=302)
assert resp.location.endswith('/login/?next=http%3A//testserver/priv/')
def test_page_private_logged_in(app, admin_user):
@ -351,6 +361,44 @@ def test_page_private_logged_in(app, admin_user):
app.get('/', status=200)
def test_page_private_all_private(app):
Page.objects.all().delete()
Page.objects.create(title='index', slug='index', public=False)
p2 = Page.objects.create(title='index2', slug='index2', public=False)
resp = app.get('/', status=403)
assert 'You do not have access to this page' not in resp
# even with a public 403 page, the error should remain the same
Page.objects.create(title='Title of the custom 403 page', slug='403', public=True, parent=p2)
resp = app.get('/', status=403)
assert 'Title of the custom 403 page' not in resp
def test_page_private_logged_in_no_perm(app, normal_user):
group_ok = Group.objects.create(name='g1')
Page.objects.all().delete()
Page.objects.create(title='index', slug='index', public=True)
priv = Page.objects.create(title='index2', slug='priv', public=False)
priv.groups.set([group_ok])
normal_user.groups.set([])
app = login(app, username='normal-user', password='normal-user')
# Falling on default small 403 template proposing to switch account
resp = app.get('/priv/', status=403)
assert 'You do not have access to this page' in resp
assert 'authenticate yourself using another account' in resp
# using a custom empty page as 403 error page
Page.objects.create(title='Title of the custom 403 page', slug='403', public=True)
resp = app.get('/priv/', status=403)
assert 'You do not have access to this page' not in resp
assert 'authenticate yourself using another account' not in resp
assert 'Title of the custom 403 page' in resp
def test_page_skeleton(app):
Page.objects.all().delete()
page = Page(