WIP: public: add error403 view and make publish_page use it (#8122) #234

Draft
yweber wants to merge 1 commits from wip/8122-error403-view into main
7 changed files with 131 additions and 80 deletions

View File

@ -30,8 +30,8 @@ from django.http import (
from django.urls import reverse
from django.utils.encoding import force_str
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from rest_framework import permissions
from rest_framework.decorators import api_view
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
@ -55,74 +55,68 @@ def dashboard_success(request, dashboard, cell_data):
return HttpResponseRedirect(dashboard_url)
class DashboardAddTileView(View):
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
raise PermissionDenied()
@api_view(['GET'])
def dashboard_add_tile(request, **kwargs):
if not request.user.is_authenticated:
raise PermissionDenied()
dashboard = DashboardCell.objects.filter(page__snapshot__isnull=True).first()
if dashboard is None:
raise Http404()
cell = CellBase.get_cell(kwargs['cell_reference'])
if not cell.page.is_visible(request.user):
raise PermissionDenied()
if not cell.is_visible(request):
raise PermissionDenied()
cell.pk = None
cell.page = dashboard.page
cell.placeholder = '_dashboard'
cell.save()
dashboard = DashboardCell.objects.filter(page__snapshot__isnull=True).first()
if dashboard is None:
raise Http404()
cell = CellBase.get_cell(kwargs['cell_reference'])
if not cell.page.is_visible(request.user):
raise PermissionDenied()
if not cell.is_visible(request):
raise PermissionDenied()
cell.pk = None
cell.page = dashboard.page
cell.placeholder = '_dashboard'
cell.save()
tile = Tile(dashboard=dashboard, cell=cell, user=request.user, order=0)
if settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'first':
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Min('order'))
.get('order__min')
)
tile.order = order - 1 if order is not None else 0
elif settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'last':
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Max('order'))
.get('order__max')
)
tile.order = order + 1 if order is not None else 0
tile.save()
cell_data = get_cell_data(cell)
cell_data['remove_url'] = reverse(
'combo-dashboard-remove-tile', kwargs={'cell_reference': cell.get_reference()}
tile = Tile(dashboard=dashboard, cell=cell, user=request.user, order=0)
if settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'first':
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Min('order'))
.get('order__min')
)
return dashboard_success(request, dashboard, cell_data)
dashboard_add_tile = DashboardAddTileView.as_view()
class DashboardRemoveTileView(View):
def get(self, request, *args, **kwargs):
cell = CellBase.get_cell(kwargs['cell_reference'])
try:
tile = Tile.get_by_cell(cell)
except Tile.DoesNotExist:
raise Http404()
if tile.user != request.user:
raise PermissionDenied()
dashboard = tile.dashboard
cell_data = get_cell_data(cell)
tile.delete()
# do not remove cell so it can directly be added back
cell_data['add_url'] = reverse(
'combo-dashboard-add-tile', kwargs={'cell_reference': cell.get_reference()}
tile.order = order - 1 if order is not None else 0
elif settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'last':
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Max('order'))
.get('order__max')
)
tile.order = order + 1 if order is not None else 0
tile.save()
return dashboard_success(request, dashboard, cell_data)
cell_data = get_cell_data(cell)
cell_data['remove_url'] = reverse(
'combo-dashboard-remove-tile', kwargs={'cell_reference': cell.get_reference()}
)
return dashboard_success(request, dashboard, cell_data)
dashboard_remove_tile = DashboardRemoveTileView.as_view()
@api_view(['GET'])
def dashboard_remove_tile(request, **kwargs):
cell = CellBase.get_cell(kwargs['cell_reference'])
try:
tile = Tile.get_by_cell(cell)
except Tile.DoesNotExist:
raise Http404()
if tile.user != request.user:
raise PermissionDenied()
dashboard = tile.dashboard
cell_data = get_cell_data(cell)
tile.delete()
# do not remove cell so it can directly be added back
cell_data['add_url'] = reverse(
'combo-dashboard-add-tile', kwargs={'cell_reference': cell.get_reference()}
)
return dashboard_success(request, dashboard, cell_data)
@csrf_exempt
@ -162,6 +156,7 @@ def dashboard_auto_tile(request, *args, **kwargs):
return response
@api_view(['GET'])
def dashboard_reorder_tiles(request, *args, **kwargs):
new_order = request.GET['order'].split(',')
tiles = {str(x.id): x for x in Tile.objects.filter(id__in=new_order)}

View File

@ -92,9 +92,9 @@ class TrackingCodeView(View):
try:
url = self.search(code, request, wcs_site=cell.wcs_site)
except PermissionDenied:
except PermissionDenied as expt:
if redirect_to_other_domain:
raise
return HttpResponseForbidden('%s' % expt)
messages.error(self.request, _('Looking up tracking code is currently rate limited.'))
else:
if url:

View File

@ -0,0 +1,14 @@
{% extends "combo/page_template.html" %}
{% load i18n %}
{% block combo-content %}
<div>
<h2>{% trans "You do not have sufficient rights to view this page" %}</h2>
<p>
{% trans "Maybe you can" %}
<a href="{% url 'auth_logout' %}?next={{ login_url | urlencode }}">
{% trans "authenticate yourself using another account ?" %}
</a>
</p>
</div>
{% endblock %}

View File

@ -44,7 +44,7 @@ from django.utils.encoding import force_str
from django.utils.http import urlencode
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt
from django.views.defaults import page_not_found
from django.views.defaults import page_not_found, permission_denied
from combo import utils
from combo.apps.assets.models import Asset
@ -120,23 +120,23 @@ def ajax_page_cell(request, page_pk, cell_reference):
raise Http404()
# as it's from a snapshot access is limited to managers
if not (request.user and request.user.is_superuser):
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
if not page.is_visible(request.user):
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
try:
cell = CellBase.get_cell(cell_reference, page_id=page_pk)
except ObjectDoesNotExist:
raise Http404()
if not cell.is_visible(request):
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
exception = None
action_response = None
if request.method == 'POST':
if not hasattr(cell, 'post'):
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
try:
action_response = cell.post(request)
except PostException as e:
@ -198,7 +198,10 @@ def render_cell(request, cell):
# Cell can pass data through its own __dict__
cell.modify_global_context(context, request)
response_content = engines['django'].from_string('{% render_cell cell %}').render(context, request)
try:
response_content = engines['django'].from_string('{% render_cell cell %}').render(context, request)
except PermissionDenied:
return permission_denied(request, exception=PermissionDenied)
response = HttpResponse(response_content, content_type='text/html')
if hasattr(request, 'page_title_from_cell'):
# cell request a change to page title, pass info in response header
@ -272,7 +275,7 @@ def skeleton(request):
# {% endblock %}
# {% endblock %}
if 'source' not in request.GET:
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
source = request.GET['source']
if 'Accept-Language' in request.headers:
@ -335,7 +338,7 @@ def skeleton(request):
continue
break
else:
raise PermissionDenied()
return permission_denied(request, exception=PermissionDenied)
# add default ParentContentCells to the page
cells = []
@ -544,10 +547,6 @@ def publish_page(request, page, status=200, template_name=None):
pages = page.get_parents_and_self()
if not page.is_visible(request.user):
if not request.user.is_authenticated:
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(request.build_absolute_uri())
raise PermissionDenied()
if page.redirect_url:
@ -626,6 +625,23 @@ def publish_page(request, page, status=200, template_name=None):
return HttpResponse(response_content, status=status)
def error403(request, *args, **kwargs):
if not request.user.is_authenticated:
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(request.build_absolute_uri())
try:
page = Page.objects.get(slug='403')
template_name = None
except Page.DoesNotExist:
page = Page.objects.filter(slug='index', parent=None).first() or Page()
page.redirect_url = None
page.public = True
page.template_name = 'standard'
template_name = 'combo/403.html'
return publish_page(request, page, status=403, template_name=template_name)
def error404(request, *args, **kwargs):
if not args and 'exception' not in kwargs:
# happens when /404 is called on portal agent

View File

@ -23,17 +23,19 @@ from django.views.i18n import JavaScriptCatalog
from . import plugins
from .manager.urls import urlpatterns as combo_manager_urls
from .public.views import error404, login, logout, mellon_page_hook
from .public.views import error403, error404, login, logout, mellon_page_hook
from .urls_utils import decorated_includes, manager_required
urlpatterns = [
re_path(r'^manage/', decorated_includes(manager_required, include(combo_manager_urls))),
re_path(r'^logout/$', logout, name='auth_logout'),
re_path(r'^login/$', login, name='auth_login'),
re_path(r'^403$', error403),
re_path(r'^404$', error404),
re_path(r'^jsi18n$', JavaScriptCatalog.as_view(), name='javascript-catalog'),
]
handler403 = error403
handler404 = error404
if 'mellon' in settings.INSTALLED_APPS:

View File

@ -1358,7 +1358,7 @@ def test_chartng_cell_view(app, normal_user, statistics):
page.public = False
page.save()
resp = app.get(location + '?width=400', status=403)
resp = app.get(location + '?width=400', status=302)
page.public = True
page.save()
@ -1367,10 +1367,10 @@ def test_chartng_cell_view(app, normal_user, statistics):
cell.public = False
cell.groups.set([group])
cell.save()
resp = app.get(location + '?width=400', status=403)
resp = app.get(location + '?width=400', status=302)
app = login(app, username='normal-user', password='normal-user')
resp = app.get(location + '?width=400', status=403)
resp = app.get(location + '?width=400', status=403) # once logged in we got 403
normal_user.groups.set([group])
normal_user.save()

View File

@ -352,6 +352,30 @@ def test_page_private_logged_in(app, admin_user):
app.get('/', status=200)
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 sufficient rights to view 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 sufficient rights 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(