public: change permission denied handling (#8122)
gitea/combo/pipeline/head This commit looks good Details

If all root pages are private and the user is not logged in :
- display standard django PermissionDenied error

Else if permissions is denied (and not "403" page exists) :
- if the user is not logged in redirect to login page
- else (user logged in) : display minimal 403 template inviting to
  logout/login

Else if a page with "403" as slug exists and permission is denied on a page :
- if the user is not logged in
  - if 403 page is public display it
  - else redirect to login page
- else (user logged in)
  - if the 403 page is visible display it
  - else display minimal 403 template
This commit is contained in:
Yann Weber 2024-01-23 11:37:51 +01:00
parent 12a3d5b392
commit 46ecf07ff0
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 %}
<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
# be viewed, display generic django Permission Denied error
raise PermissionDenied()
return redirect_to_login(request.build_absolute_uri())
raise PermissionDenied()
try:
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
# 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:
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
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(