From 57938b039df45e698886e840a728d55102e97327 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Thu, 6 Aug 2020 10:59:52 +0000 Subject: [PATCH] Converted many cases of plain-text 403 messages to use a properly styled page instead, with a login link when appropriate. Also changed some API endpoint 400 responses to a more correct 403. - Legacy-Id: 18339 --- ietf/api/tests.py | 8 ++-- ietf/community/views.py | 9 +++-- ietf/dbtemplate/views.py | 11 ++++-- ietf/doc/tests_ballot.py | 2 +- ietf/doc/views_ballot.py | 9 +++-- ietf/doc/views_charter.py | 13 +++--- ietf/doc/views_doc.py | 7 ++-- ietf/doc/views_draft.py | 33 ++++++++-------- ietf/doc/views_material.py | 9 +++-- ietf/doc/views_review.py | 25 ++++++------ ietf/group/admin.py | 4 +- ietf/group/milestones.py | 10 ++--- ietf/group/views.py | 23 +++++------ ietf/ietfauth/tests.py | 2 +- ietf/ietfauth/utils.py | 7 ++-- ietf/liaisons/views.py | 17 ++++---- ietf/meeting/views.py | 70 +++++++++++++++++---------------- ietf/nomcom/views.py | 25 ++++++------ ietf/person/models.py | 1 + ietf/secr/announcement/views.py | 10 +++-- ietf/secr/utils/decorators.py | 8 ++-- ietf/stats/views.py | 5 ++- ietf/submit/views.py | 15 +++---- ietf/sync/views.py | 10 +++-- ietf/templates/403.html | 3 -- ietf/utils/decorators.py | 2 +- ietf/utils/response.py | 3 +- 27 files changed, 183 insertions(+), 158 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 4d7fb91cb..89f9a67ee 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -83,7 +83,7 @@ class CustomApiTests(TestCase): badrole.person.user.last_login = timezone.now() badrole.person.user.save() r = self.client.post(url, {'apikey': badapikey.hash()} ) - self.assertContains(r, "Restricted to role Recording Manager", status_code=403) + self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) r = self.client.post(url, {'apikey': apikey.hash()} ) self.assertContains(r, "Too long since last regular login", status_code=400) @@ -173,7 +173,7 @@ class CustomApiTests(TestCase): badrole.person.user.last_login = timezone.now() badrole.person.user.save() r = self.client.post(url, {'apikey': badapikey.hash()} ) - self.assertContains(r, "Restricted to roles Recording Manager, Secretariat", status_code=403) + self.assertContains(r, "Restricted to roles: Recording Manager, Secretariat", status_code=403) r = self.client.post(url, {'apikey': apikey.hash()} ) self.assertContains(r, "Too long since last regular login", status_code=400) @@ -257,7 +257,7 @@ class CustomApiTests(TestCase): badrole.person.user.last_login = timezone.now() badrole.person.user.save() r = self.client.post(url, {'apikey': badapikey.hash()}) - self.assertContains(r, "Restricted to role Secretariat", status_code=403) + self.assertContains(r, "Restricted to role: Secretariat", status_code=403) r = self.client.post(url, {'apikey': apikey.hash()}) self.assertContains(r, "Too long since last regular login", status_code=400) @@ -292,7 +292,7 @@ class CustomApiTests(TestCase): } url = urlreverse('ietf.api.views.api_new_meeting_registration') r = self.client.post(url, reg) - self.assertContains(r, 'Invalid apikey', status_code=400) + self.assertContains(r, 'Invalid apikey', status_code=403) oidcp = PersonFactory(user__is_staff=True) # Make sure 'oidcp' has an acceptable role RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat') diff --git a/ietf/community/views.py b/ietf/community/views.py index eb4c8efd7..0e0dcb9a0 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -7,7 +7,7 @@ import datetime import json import uuid -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404, render from django.contrib.auth.decorators import login_required from django.utils.html import strip_tags @@ -21,6 +21,7 @@ from ietf.community.utils import docs_tracked_by_community_list, docs_matching_c from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule from ietf.doc.models import DocEvent, Document from ietf.doc.utils_search import prepare_document_table +from ietf.utils.response import permission_denied def view_list(request, username=None): clist = lookup_community_list(username) @@ -45,7 +46,7 @@ def manage_list(request, username=None, acronym=None, group_type=None): clist = lookup_community_list(username, acronym) if not can_manage_community_list(request.user, clist): - return HttpResponseForbidden("You do not have permission to access this view") + permission_denied(request, "You do not have permission to access this view") action = request.POST.get('action') @@ -129,7 +130,7 @@ def track_document(request, name, username=None, acronym=None): if request.method == "POST": clist = lookup_community_list(username, acronym) if not can_manage_community_list(request.user, clist): - return HttpResponseForbidden("You do not have permission to access this view") + permission_denied(request, "You do not have permission to access this view") if clist.pk is None: clist.save() @@ -151,7 +152,7 @@ def untrack_document(request, name, username=None, acronym=None): doc = get_object_or_404(Document, docalias__name=name) clist = lookup_community_list(username, acronym) if not can_manage_community_list(request.user, clist): - return HttpResponseForbidden("You do not have permission to access this view") + permission_denied(request, "You do not have permission to access this view") if request.method == "POST": if clist.pk is not None: diff --git a/ietf/dbtemplate/views.py b/ietf/dbtemplate/views.py index 511062a88..11b1a077e 100644 --- a/ietf/dbtemplate/views.py +++ b/ietf/dbtemplate/views.py @@ -1,4 +1,6 @@ -from django.http import HttpResponseForbidden, HttpResponseRedirect +# Copyright The IETF Trust 2012-2020, All Rights Reserved + +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render import debug # pyflakes:ignore @@ -7,13 +9,14 @@ from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.forms import DBTemplateForm from ietf.group.models import Group from ietf.ietfauth.utils import has_role +from ietf.utils.response import permission_denied def group_template_list(request, acronym): group = get_object_or_404(Group, acronym=acronym) chairs = group.role_set.filter(name__slug='chair') if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): - return HttpResponseForbidden("You are not authorized to access this view") + permission_denied(request, "You are not authorized to access this view.") template_list = DBTemplate.objects.filter(group=group) return render(request, 'dbtemplate/template_list.html', @@ -28,7 +31,7 @@ def group_template_edit(request, acronym, template_id, base_template='dbtemplate extra_context = extra_context or {} if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): - return HttpResponseForbidden("You are not authorized to access this view") + permission_denied(request, "You are not authorized to access this view.") template = get_object_or_404(DBTemplate, id=template_id, group=group) if request.method == 'POST': @@ -52,7 +55,7 @@ def group_template_show(request, acronym, template_id, base_template='dbtemplate extra_context = extra_context or {} if not has_role(request.user, "Secretariat") and not (request.user.id and chairs.filter(person__user=request.user).count()): - return HttpResponseForbidden("You are not authorized to access this view") + permission_denied(request, "You are not authorized to access this view.") template = get_object_or_404(DBTemplate, id=template_id, group=group) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 76aee087c..3c1f670e3 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -739,7 +739,7 @@ class ApproveBallotTests(TestCase): # Only Secretariat can use this URL login_testing_unauthorized(self, "ad", url) r = self.client.get(url) - self.assertContains(r, "Restricted to role Secretariat", status_code=403) + self.assertContains(r, "Restricted to role: Secretariat", status_code=403) # There are no downrefs, the page should say so login_testing_unauthorized(self, "secretary", url) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 4962afc2b..2ba13ed66 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -8,7 +8,7 @@ import datetime, json from django import forms from django.conf import settings -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import render, get_object_or_404, redirect from django.template.defaultfilters import striptags from django.template.loader import render_to_string @@ -28,6 +28,7 @@ from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred, generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote, generate_approval_mail, email_irsg_ballot_closed, email_irsg_ballot_issued ) from ietf.doc.lastcall import request_last_call +from ietf.doc.templatetags.ietf_filters import can_ballot from ietf.iesg.models import TelechatDate from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_stream from ietf.mailtrigger.utils import gather_address_lists @@ -38,7 +39,7 @@ from ietf.person.models import Person from ietf.utils import log from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.utils.decorators import require_api_key -from ietf.doc.templatetags.ietf_filters import can_ballot +from ietf.utils.response import permission_denied BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -213,7 +214,7 @@ def edit_position(request, name, ballot_id): old_pos = None if not has_role(request.user, "Secretariat") and not can_ballot(request.user, doc): # prevent pre-ADs from voting - return HttpResponseForbidden("Must be a proper Area Director in an active area or IRSG Member to cast ballot") + permission_denied(request, "Must be a proper Area Director in an active area or IRSG Member to cast ballot") form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type) if form.is_valid(): @@ -682,7 +683,7 @@ def ballot_rfceditornote(request, name): doc = get_object_or_404(Document, docalias__name=name) if not is_authorized_in_doc_stream(request.user, doc): - return HttpResponseForbidden("You do not have the necessary permissions to change the RFC Editor Note for this document") + permission_denied(request, "You do not have the necessary permissions to change the RFC Editor Note for this document") login = request.user.person diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index 916b69c36..64cfb185f 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -8,7 +8,7 @@ import json import os import textwrap -from django.http import HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404 +from django.http import HttpResponseRedirect, HttpResponseNotFound, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse as urlreverse from django import forms @@ -32,16 +32,17 @@ from ietf.doc.utils_charter import ( historic_milestones_for_charter, change_group_state_after_charter_approval, fix_charter_revision_after_approval, split_charter_name) from ietf.doc.mails import email_state_changed, email_charter_internal_review +from ietf.group.mails import email_admin_re_charter from ietf.group.models import Group, ChangeStateGroupEvent, MilestoneGroupEvent from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_group_type +from ietf.group.views import fill_in_charter_info from ietf.ietfauth.utils import has_role, role_required from ietf.name.models import GroupStateName from ietf.person.models import Person from ietf.utils.history import find_history_active_at from ietf.utils.mail import send_mail_preformatted from ietf.utils.textupload import get_cleaned_text_file_content -from ietf.group.mails import email_admin_re_charter -from ietf.group.views import fill_in_charter_info +from ietf.utils.response import permission_denied class ChangeStateForm(forms.Form): charter_state = forms.ModelChoiceField(State.objects.filter(used=True, type="charter"), label="Charter state", empty_label=None, required=False) @@ -70,7 +71,7 @@ def change_state(request, name, option=None): group = charter.group if not can_manage_group_type(request.user, group): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view.") chartering_type = get_chartering_type(charter) @@ -261,7 +262,7 @@ def change_title(request, name, option=None): charter = get_object_or_404(Document, type="charter", name=name) group = charter.group if not can_manage_group_type(request.user, group): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view.") by = request.user.person if request.method == 'POST': form = ChangeTitleForm(request.POST, charter=charter) @@ -374,7 +375,7 @@ def submit(request, name, option=None): charter_rev = "00-00" if not can_manage_group_type(request.user, group) or not group.features.has_chartering_process: - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view.") path = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter_canonical_name, charter_rev)) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 19d42394e..94dcb0a37 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -43,7 +43,7 @@ import re from urllib.parse import quote -from django.http import HttpResponse, Http404 , HttpResponseForbidden +from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse as urlreverse @@ -76,6 +76,7 @@ from ietf.review.models import ReviewAssignment from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs from ietf.review.utils import no_review_from_teams_on_doc from ietf.utils import markup_txt +from ietf.utils.response import permission_denied from ietf.utils.text import maybe_split @@ -1199,7 +1200,7 @@ def add_comment(request, name): can_add_comment = has_role(request.user, ("Area Director", "Secretariat", "IRTF Chair")) if not can_add_comment: # The user is a chair or secretary, but not for this WG or RG - return HttpResponseForbidden("You need to be a chair or secretary of this group to add a comment.") + permission_denied(request, "You need to be a chair or secretary of this group to add a comment.") if request.method == 'POST': form = AddCommentForm(request.POST) @@ -1272,7 +1273,7 @@ def edit_notify(request, name): doc = get_object_or_404(Document, name=name) if not ( is_authorized_in_doc_stream(request.user, doc) or user_is_person(request.user, doc.shepherd and doc.shepherd.person) or has_role(request.user, ["Area Director"]) ): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") init = { "notify" : doc.notify } diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index f780bfc5e..169b61635 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -15,7 +15,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.db.models import Q -from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404 +from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.forms.utils import ErrorList @@ -41,6 +41,7 @@ from ietf.group.models import Group, Role, GroupFeatures from ietf.iesg.models import TelechatDate from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, is_individual_draft_author from ietf.ietfauth.utils import role_required +from ietf.mailtrigger.utils import gather_address_lists from ietf.message.models import Message from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, ExtResourceName from ietf.person.fields import SearchableEmailField @@ -49,7 +50,7 @@ from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.validators import validate_external_resource_value from ietf.utils import log -from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.response import permission_denied class ChangeStateForm(forms.Form): state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True) @@ -267,7 +268,7 @@ def change_stream(request, name): Role.objects.filter(name="chair", group__acronym__in=StreamName.objects.values_list("slug", flat=True), person__user=request.user))): - return HttpResponseForbidden("You do not have permission to view this page") + permission_denied(request, "You do not have permission to view this page") login = request.user.person @@ -344,7 +345,7 @@ def replaces(request, name): raise Http404 if not (has_role(request.user, ("Secretariat", "Area Director", "WG Chair", "RG Chair", "WG Secretary", "RG Secretary")) or is_authorized_in_doc_stream(request.user, doc)): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") if request.method == 'POST': form = ReplacesForm(request.POST, doc=doc) @@ -388,7 +389,7 @@ def review_possibly_replaces(request, name): raise Http404 if not (has_role(request.user, ("Secretariat", "Area Director")) or is_authorized_in_doc_stream(request.user, doc)): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page") suggested = list(doc.related_that_doc("possibly-replaces")) if not suggested: @@ -444,7 +445,7 @@ def change_intention(request, name): if not (has_role(request.user, ("Secretariat", "Area Director")) or is_authorized_in_doc_stream(request.user, doc)): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") login = request.user.person @@ -928,7 +929,7 @@ def edit_shepherd_writeup(request, name): or has_role(request.user, ["Area Director"])) if not can_edit_shepherd_writeup: - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page") login = request.user.person @@ -996,7 +997,7 @@ def edit_shepherd(request, name): can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc) if not can_edit_stream_info: - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") if request.method == 'POST': form = ShepherdForm(request.POST) @@ -1055,7 +1056,7 @@ def change_shepherd_email(request, name): can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc) is_shepherd = user_is_person(request.user, doc.shepherd and doc.shepherd.person) if not can_edit_stream_info and not is_shepherd: - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page") initial = { "shepherd": doc.shepherd_id } if request.method == 'POST': @@ -1147,7 +1148,7 @@ def edit_consensus(request, name): if not (has_role(request.user, ("Secretariat", "Area Director")) or is_authorized_in_doc_stream(request.user, doc)): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") prev_consensus = e.consensus if e else default_consensus(doc) @@ -1159,7 +1160,7 @@ def edit_consensus(request, name): e = ConsensusDocEvent(doc=doc, rev=doc.rev, type="changed_consensus", by=request.user.person) e.consensus = {"Unknown":None,"Yes":True,"No":False}[form.cleaned_data["consensus"]] if not e.consensus and doc.intended_std_level_id in ("std", "ds", "ps", "bcp"): - return HttpResponseForbidden("BCPs and Standards Track documents must include the consensus boilerplate") + permission_denied(request, "BCPs and Standards Track documents must include the consensus boilerplate.") e.desc = "Changed consensus to %s from %s" % (nice_consensus(e.consensus), nice_consensus(prev_consensus)) @@ -1224,7 +1225,7 @@ def edit_doc_extresources(request, name): if not (has_role(request.user, ("Secretariat", "Area Director")) or is_authorized_in_doc_stream(request.user, doc) or is_individual_draft_author(request.user, doc)): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") old_resources = format_resources(doc.docextresource_set.all()) @@ -1270,7 +1271,7 @@ def request_publication(request, name): doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf")) if not is_authorized_in_doc_stream(request.user, doc): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") consensus_event = doc.latest_event(ConsensusDocEvent, type="changed_consensus") @@ -1410,7 +1411,7 @@ def adopt_draft(request, name): doc = get_object_or_404(Document, type="draft", name=name) if not can_adopt_draft(request.user, doc): - return HttpResponseForbidden("You don't have permission to access this page") + permission_denied(request, "You don't have permission to access this page.") if request.method == 'POST': form = AdoptDraftForm(request.POST, user=request.user) @@ -1501,7 +1502,7 @@ def release_draft(request, name): raise Http404 if not can_unadopt_draft(request.user, doc): - return HttpResponseForbidden("You don't have permission to access this page") + permission_denied(request, "You don't have permission to access this page.") if request.method == 'POST': form = ReleaseDraftForm(request.POST) @@ -1632,7 +1633,7 @@ def change_stream_state(request, name, state_type): state_type = get_object_or_404(StateType, slug=state_type) if not is_authorized_in_doc_stream(request.user, doc): - return HttpResponseForbidden("You don't have permission to access this page") + permission_denied(request, "You don't have permission to access this page.") prev_state = doc.get_state(state_type.slug) next_states = next_states_for_stream_state(doc, state_type, prev_state) diff --git a/ietf/doc/views_material.py b/ietf/doc/views_material.py index 46a24fd90..60f9dbc1a 100644 --- a/ietf/doc/views_material.py +++ b/ietf/doc/views_material.py @@ -8,10 +8,10 @@ import os import re from django import forms -from django.shortcuts import render, get_object_or_404, redirect -from django.http import HttpResponseForbidden, Http404 -from django.utils.html import mark_safe # type:ignore from django.contrib.auth.decorators import login_required +from django.http import Http404 +from django.shortcuts import render, get_object_or_404, redirect +from django.utils.html import mark_safe # type:ignore from django.urls import reverse as urlreverse import debug # pyflakes:ignore @@ -21,6 +21,7 @@ from ietf.doc.models import NewRevisionDocEvent from ietf.doc.utils import add_state_change_event, check_common_doc_name_rules from ietf.group.models import Group from ietf.group.utils import can_manage_materials +from ietf.utils.response import permission_denied @login_required def choose_material_type(request, acronym): @@ -106,7 +107,7 @@ def edit_material(request, name=None, acronym=None, action=None, doc_type=None): if not can_manage_materials(request.user, group): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view") if request.method == 'POST': form = UploadMaterialForm(document_type, action, group, doc, request.POST, request.FILES) diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index 47bb1d441..fc2ef8aef 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -15,7 +15,7 @@ from simple_history.utils import update_change_reason import debug # pyflakes:ignore -from django.http import HttpResponseForbidden, JsonResponse, Http404, HttpResponse, HttpResponseRedirect +from django.http import JsonResponse, Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import render, get_object_or_404, redirect from django import forms from django.conf import settings @@ -49,6 +49,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.mail import send_mail_message from ietf.mailtrigger.utils import gather_address_lists from ietf.utils.fields import MultiEmailField +from ietf.utils.response import permission_denied def clean_doc_revision(doc, rev): if rev: @@ -112,7 +113,7 @@ def request_review(request, name): doc = get_object_or_404(Document, name=name) if not can_request_review_of_doc(request.user, doc): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") now = datetime.datetime.now() @@ -280,7 +281,7 @@ def close_request(request, name, request_id): can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team) if not (can_request or can_manage_request): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST": form = CloseReviewRequestForm(can_manage_request, request.POST) @@ -315,7 +316,7 @@ def assign_reviewer(request, name, request_id): review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "assigned"]) if not can_manage_review_requests_for_team(request.user, review_req.team): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST" and request.POST.get("action") == "assign": form = AssignReviewerForm(review_req, request.POST) @@ -351,7 +352,7 @@ def reject_reviewer_assignment(request, name, assignment_id): can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team) if not (is_reviewer or can_manage_request): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST" and request.POST.get("action") == "reject" and not review_request_past_deadline: form = RejectReviewerAssignmentForm(request.POST) @@ -406,7 +407,7 @@ def withdraw_reviewer_assignment(request, name, assignment_id): can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team) if not can_manage_request: - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST" and request.POST.get("action") == "withdraw": review_assignment.state_id = 'withdrawn' @@ -447,7 +448,7 @@ def mark_reviewer_assignment_no_response(request, name, assignment_id): can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team) if not can_manage_request: - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST" and request.POST.get("action") == "noresponse": review_assignment.state_id = 'no-response' @@ -649,7 +650,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): can_manage_request = can_manage_review_requests_for_team(request.user, assignment.review_request.team) if not (is_reviewer or can_manage_request): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") team = assignment.review_request.team team_acronym = assignment.review_request.team.acronym.lower() @@ -666,7 +667,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): else: team = get_object_or_404(Group, acronym=acronym) if not can_manage_review_requests_for_team(request.user, team): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") assignment = None is_reviewer = False revising_review = False @@ -918,7 +919,7 @@ def search_mail_archive(request, name, acronym=None, assignment_id=None): can_manage_request = can_manage_review_requests_for_team(request.user, team) if not (is_reviewer or can_manage_request): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") res = mailarch.construct_query_urls(doc, team, query=request.GET.get("query")) if not res: @@ -951,7 +952,7 @@ class EditReviewRequestCommentForm(forms.ModelForm): def edit_comment(request, name, request_id): review_req = get_object_or_404(ReviewRequest, pk=request_id) if not can_request_review_of_doc(request.user, review_req.doc): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") if request.method == "POST": form = EditReviewRequestCommentForm(request.POST, instance=review_req) @@ -982,7 +983,7 @@ class EditReviewRequestDeadlineForm(forms.ModelForm): def edit_deadline(request, name, request_id): review_req = get_object_or_404(ReviewRequest, pk=request_id) if not can_request_review_of_doc(request.user, review_req.doc): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") old_deadline = review_req.deadline diff --git a/ietf/group/admin.py b/ietf/group/admin.py index 90f17b0fc..d3e075145 100644 --- a/ietf/group/admin.py +++ b/ietf/group/admin.py @@ -9,7 +9,6 @@ from django import forms from django.contrib import admin from django.contrib.admin.utils import unquote -from django.core.exceptions import PermissionDenied from django.core.management import load_command_class from django.http import Http404 from django.shortcuts import render @@ -22,6 +21,7 @@ from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, G MilestoneGroupEvent, GroupExtResource, ) from ietf.utils.validators import validate_external_resource_value +from ietf.utils.response import permission_denied class RoleInline(admin.TabularInline): model = Role @@ -121,7 +121,7 @@ class GroupAdmin(admin.ModelAdmin): obj = None if not self.has_change_permission(request, obj): - raise PermissionDenied + permission_denied(request, "You don't have edit permissions for this change.") if obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)}) diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py index 92531f4ef..643dcac97 100644 --- a/ietf/group/milestones.py +++ b/ietf/group/milestones.py @@ -6,8 +6,7 @@ import calendar from django import forms from django.contrib import messages -from django.core.exceptions import PermissionDenied -from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, Http404 +from django.http import HttpResponseRedirect, HttpResponseBadRequest, Http404 from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required @@ -22,6 +21,7 @@ from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, from ietf.name.models import GroupMilestoneStateName from ietf.group.mails import email_milestones_changed from ietf.utils.fields import DatepickerDateField +from ietf.utils.response import permission_denied class MilestoneForm(forms.Form): id = forms.IntegerField(required=True, widget=forms.HiddenInput) @@ -118,7 +118,7 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"): if milestone_set == "current": needs_review = True else: - return HttpResponseForbidden("You are not authorized to edit the milestones of this group.") + permission_denied(request, "You are not authorized to edit the milestones of this group.") desc_editable = has_role(request.user,["Secretariat","Area Director","IRTF Chair"]) @@ -320,7 +320,7 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"): for m in milestones: forms.append(MilestoneForm(needs_review, reviewer, desc_editable, instance=m, uses_dates=group.uses_milestone_dates)) else: - raise PermissionDenied + permission_denied(request, "You don't have the required permissions to change the 'uses milestone dates' setting") else: # parse out individual milestone forms for prefix in request.POST.getlist("prefix"): @@ -405,7 +405,7 @@ def reset_charter_milestones(request, group_type, acronym): raise Http404 if not can_manage_group(request.user, group): - return HttpResponseForbidden("You are not authorized to change the milestones for this group.") + permission_denied(request, "You are not authorized to change the milestones for this group.") current_milestones = group.groupmilestone_set.filter(state="active") charter_milestones = group.groupmilestone_set.filter(state="charter") diff --git a/ietf/group/views.py b/ietf/group/views.py index 42f188135..d12f7ef74 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -51,7 +51,7 @@ from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.db.models import Q, Count -from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect, JsonResponse +from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse from django.shortcuts import render, redirect, get_object_or_404 from django.template.loader import render_to_string from django.urls import reverse as urlreverse @@ -119,6 +119,7 @@ from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.models import Recipient from ietf.settings import MAILING_LIST_INFO_URL from ietf.utils.pipe import pipe +from ietf.utils.response import permission_denied from ietf.utils.text import strip_suffix @@ -853,7 +854,7 @@ def group_photos(request, group_type=None, acronym=None): # group_type = group.type_id # # if not can_manage_group(request.user, group): -# return HttpResponseForbidden("You don't have permission to access this view") +# permission_denied(request, "You don't have permission to access this view") # # if not group.charter: # group.charter = get_or_create_initial_charter(group, group_type) @@ -905,7 +906,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None): group_type = group.type_id if not (can_manage_group(request.user, group) or group.has_role(request.user, group.features.groupman_roles)): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view") if request.method == 'POST': form = GroupForm(request.POST, group=group, group_type=group_type, field=field) @@ -1089,7 +1090,7 @@ def conclude(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not can_manage_group_type(request.user, group): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view") if request.method == 'POST': form = ConcludeGroupForm(request.POST) @@ -1136,7 +1137,7 @@ def customize_workflow(request, group_type=None, acronym=None): if not (can_manage_group(request.user, group) or group.has_role(request.user, group.features.groupman_roles)): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view") if group_type == "rg": stream_id = "irtf" @@ -1246,7 +1247,7 @@ def stream_edit(request, acronym): group = get_object_or_404(Group, acronym=acronym) if not (has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")): - return HttpResponseForbidden("You don't have permission to access this page.") + permission_denied(request, "You don't have permission to access this page.") chairs = Email.objects.filter(role__group=group, role__name="chair").select_related("person") @@ -1487,7 +1488,7 @@ def manage_review_requests(request, acronym, group_type=None, assignment_status= raise Http404 if not can_manage_review_requests_for_team(request.user, group): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") review_requests = get_open_review_requests_for_team(group, assignment_status=assignment_status) @@ -1622,7 +1623,7 @@ def email_open_review_assignments(request, acronym, group_type=None): raise Http404 if not can_manage_review_requests_for_team(request.user, group): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") review_assignments = list(ReviewAssignment.objects.filter( review_request__team=group, @@ -1731,7 +1732,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None): if not (user_is_person(request.user, reviewer) or can_manage_review_requests_for_team(request.user, group)): - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") exclude_fields = [] if not can_manage_review_requests_for_team(request.user, group): @@ -1918,7 +1919,7 @@ def add_comment(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not is_authorized_in_group(request.user,group): - return HttpResponseForbidden("You need to a chair, secretary, or delegate of this group to add a comment.") + permission_denied(request, "You need to a chair, secretary, or delegate of this group to add a comment.") if request.method == 'POST': form = AddCommentForm(request.POST) @@ -1949,7 +1950,7 @@ def reset_next_reviewer(request, acronym, group_type=None): raise Http404 if not Role.objects.filter(name="secr", group=group, person__user=request.user).exists() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("You don't have permission to access this view") + permission_denied(request, "You don't have permission to access this view") instance = group.nextreviewerinteam_set.first() if not instance: diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 4d6d19f54..644a40c7b 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -613,7 +613,7 @@ class IetfAuthTests(TestCase): # invalid apikey r = self.client.post(key.endpoint, {'apikey':BAD_KEY, 'dummy':'dummy',}) - self.assertContains(r, 'Invalid apikey', status_code=400) + self.assertContains(r, 'Invalid apikey', status_code=403) # too long since regular login person.user.last_login = datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1) diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index bfb66256c..ba4adb10d 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -14,8 +14,9 @@ from functools import wraps from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME +from django.core.exceptions import PermissionDenied from django.db.models import Q -from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.decorators import available_attrs from django.utils.http import urlquote @@ -117,7 +118,7 @@ def passes_test_decorator(test_func, message): elif test_func(request.user, *args, **kwargs): return view_func(request, *args, **kwargs) else: - return HttpResponseForbidden(message) + raise PermissionDenied(message) return inner return decorate @@ -126,7 +127,7 @@ def role_required(*role_names): """View decorator for checking that the user is logged in and has one of the listed roles.""" return passes_test_decorator(lambda u, *args, **kwargs: has_role(u, role_names, *args, **kwargs), - "Restricted to role%s %s" % ("s" if len(role_names) != 1 else "", ", ".join(role_names))) + "Restricted to role%s: %s" % ("s" if len(role_names) != 1 else "", ", ".join(role_names))) # specific permissions diff --git a/ietf/liaisons/views.py b/ietf/liaisons/views.py index 51e6225c6..78fc164dc 100644 --- a/ietf/liaisons/views.py +++ b/ietf/liaisons/views.py @@ -9,7 +9,7 @@ from django.contrib import messages from django.urls import reverse as urlreverse from django.core.validators import validate_email, ValidationError from django.db.models import Q, Prefetch -from django.http import HttpResponse, HttpResponseForbidden +from django.http import HttpResponse from django.shortcuts import render, get_object_or_404, redirect import debug # pyflakes:ignore @@ -26,6 +26,7 @@ from ietf.liaisons.forms import liaison_form_factory, SearchLiaisonForm, EditAtt from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email from ietf.liaisons.fields import select2_id_liaison_json from ietf.name.models import LiaisonStatementTagName +from ietf.utils.response import permission_denied EMAIL_ALIASES = { 'IETFCHAIR':'The IETF Chair ', @@ -322,9 +323,9 @@ def add_comment(request, object_id): @can_submit_liaison_required def liaison_add(request, type=None, **kwargs): if type == 'incoming' and not can_add_incoming_liaison(request.user): - return HttpResponseForbidden("Restricted to users who are authorized to submit incoming liaison statements") + permission_denied(request, "Restricted to users who are authorized to submit incoming liaison statements.") if type == 'outgoing' and not can_add_outgoing_liaison(request.user): - return HttpResponseForbidden("Restricted to users who are authorized to submit outgoing liaison statements") + permission_denied(request, "Restricted to users who are authorized to submit outgoing liaison statements.") if request.method == 'POST': form = liaison_form_factory(request, data=request.POST.copy(), @@ -372,7 +373,7 @@ def liaison_delete_attachment(request, object_id, attach_id): liaison = get_object_or_404(LiaisonStatement, pk=object_id) attach = get_object_or_404(LiaisonStatementAttachment, pk=attach_id) if not can_edit_liaison(request.user, liaison): - return HttpResponseForbidden("You are not authorized for this action") + permission_denied(request, "You are not authorized for this action.") # FIXME: this view should use POST instead of GET when deleting attach.removed = True @@ -430,7 +431,7 @@ def liaison_detail(request, object_id): def liaison_edit(request, object_id): liaison = get_object_or_404(LiaisonStatement, pk=object_id) if not can_edit_liaison(request.user, liaison): - return HttpResponseForbidden('You do not have permission to edit this liaison statement') + permission_denied(request, 'You do not have permission to edit this liaison statement.') return liaison_add(request, instance=liaison) def liaison_edit_attachment(request, object_id, doc_id): @@ -438,7 +439,7 @@ def liaison_edit_attachment(request, object_id, doc_id): liaison = get_object_or_404(LiaisonStatement, pk=object_id) doc = get_object_or_404(Document, pk=doc_id) if not can_edit_liaison(request.user, liaison): - return HttpResponseForbidden("You are not authorized for this action") + permission_denied(request, "You are not authorized for this action.") if request.method == 'POST': form = EditAttachmentForm(request.POST) @@ -480,8 +481,8 @@ def liaison_list(request, state='posted'): # check authorization for pending and dead tabs if state in ('pending','dead') and not can_add_liaison(request.user): - msg = "Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities" - return HttpResponseForbidden(msg) + msg = "Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities." + permission_denied(request, msg) if 'tags' in request.GET: value = request.GET.get('tags') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index f6146646f..a990478cb 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -27,7 +27,7 @@ import debug # pyflakes:ignore from django import forms from django.shortcuts import render, redirect, get_object_or_404 -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -86,10 +86,11 @@ from ietf.utils.decorators import require_api_key from ietf.utils.history import find_history_replacements_active_at from ietf.utils.log import assertion from ietf.utils.mail import send_mail_message, send_mail_text +from ietf.utils.mime import get_mime_type from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages +from ietf.utils.response import permission_denied from ietf.utils.text import xslugify -from ietf.utils.mime import get_mime_type from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,) @@ -462,7 +463,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): if not can_see: if request.method == 'POST': - return HttpResponseForbidden("Can't view this schedule") + permission_denied(request, "Can't view this schedule.") # FIXME: check this return render(request, "meeting/private_schedule.html", @@ -491,7 +492,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): if request.method == 'POST': if not can_edit: - return HttpResponseForbidden("Can't edit this schedule") + permission_denied(request, "Can't edit this schedule.") action = request.POST.get('action') @@ -859,7 +860,7 @@ def edit_schedule_properties(request, num=None, owner=None, name=None): cansee, canedit, secretariat = schedule_permissions(meeting, schedule, request.user) if not (canedit or has_role(request.user,'Secretariat')): - return HttpResponseForbidden("You may not edit this schedule") + permission_denied(request, "You may not edit this schedule.") else: if request.method == 'POST': form = SchedulePropertiesForm(instance=schedule,data=request.POST) @@ -1704,12 +1705,12 @@ def upload_session_bluesheets(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload bluesheets for this session.") + permission_denied(request, "You don't have permission to upload bluesheets for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") if session.meeting.type.slug == 'ietf' and not has_role(request.user, 'Secretariat'): - return HttpResponseForbidden('Restricted to role Secretariat') + permission_denied(request, 'Restricted to role Secretariat') session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) @@ -1801,9 +1802,9 @@ def upload_session_minutes(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload minutes for this session.") + permission_denied(request, "You don't have permission to upload minutes for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) @@ -1901,9 +1902,9 @@ def upload_session_agenda(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload an agenda for this session.") + permission_denied(request, "You don't have permission to upload an agenda for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) @@ -2015,9 +2016,9 @@ def upload_session_slides(request, session_id, num, name): # num is redundant, but we're dragging it along an artifact of where we are in the current URL structure session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload slides for this session.") + permission_denied(request, "You don't have permission to upload slides for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) @@ -2111,7 +2112,7 @@ def upload_session_slides(request, session_id, num, name): def propose_session_slides(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) @@ -2175,9 +2176,9 @@ def remove_sessionpresentation(request, session_id, num, name): sp = get_object_or_404(SessionPresentation,session_id=session_id,document__name=name) session = sp.session if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to manage materials for this session.") + permission_denied(request, "You don't have permission to manage materials for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") if request.method == 'POST': session.sessionpresentation_set.filter(pk=sp.pk).delete() c = DocEvent(type="added_comment", doc=sp.document, rev=sp.document.rev, by=request.user.person) @@ -2191,9 +2192,9 @@ def ajax_add_slides_to_session(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload slides for this session.") + permission_denied(request, "You don't have permission to upload slides for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") if request.method != 'POST' or not request.POST: return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json') @@ -2224,9 +2225,9 @@ def ajax_remove_slides_from_session(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload slides for this session.") + permission_denied(request, "You don't have permission to upload slides for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") if request.method != 'POST' or not request.POST: return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json') @@ -2262,9 +2263,9 @@ def ajax_reorder_slides_in_session(request, session_id, num): session = get_object_or_404(Session,pk=session_id) if not session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to upload slides for this session.") + permission_denied(request, "You don't have permission to upload slides for this session.") if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") if request.method != 'POST' or not request.POST: return HttpResponse(json.dumps({ 'success' : False, 'error' : 'No data submitted or not POST' }),content_type='application/json') @@ -2341,13 +2342,13 @@ def delete_schedule(request, num, owner, name): schedule = get_schedule_by_name(meeting, person, name) if schedule.name=='Empty-Schedule': - return HttpResponseForbidden('You may not delete the default empty schedule') + permission_denied(request, 'You may not delete the default empty schedule') if schedule == meeting.schedule: - return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting) + permission_denied(request, 'You may not delete the official schedule for %s'%meeting) if not ( has_role(request.user, 'Secretariat') or person.user == request.user ): - return HttpResponseForbidden("You may not delete other user's schedules") + permission_denied(request, "You may not delete other user's schedules") if request.method == 'POST': schedule.delete() @@ -2466,7 +2467,7 @@ def interim_skip_announcement(request, number): def interim_pending(request): if not can_manage_some_groups(request.user): - return HttpResponseForbidden() + permission_denied(request, "You are not authorized to access this view") '''View which shows interim meeting requests pending approval''' meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw') @@ -2489,7 +2490,7 @@ def interim_pending(request): def interim_request(request): if not can_manage_some_groups(request.user): - return HttpResponseForbidden("You don't have permission to request any interims") + permission_denied(request, "You don't have permission to request any interims") '''View for requesting an interim meeting''' SessionFormset = inlineformset_factory( @@ -2582,7 +2583,7 @@ def interim_request_cancel(request, number): first_session = meeting.session_set.first() group = first_session.group if not can_manage_group(request.user, group): - return HttpResponseForbidden("You do not have permissions to cancel this meeting request") + permission_denied(request, "You do not have permissions to cancel this meeting request") session_status = current_session_status(first_session) if request.method == 'POST': @@ -2622,7 +2623,7 @@ def interim_request_details(request, number): meeting = get_object_or_404(Meeting, number=number) group = meeting.session_set.first().group if not can_manage_group(request.user, group): - return HttpResponseForbidden("You do not have permissions to manage this meeting request") + permission_denied(request, "You do not have permissions to manage this meeting request") sessions = meeting.session_set.all() can_edit = can_edit_interim_request(meeting, request.user) can_approve = can_approve_interim_request(meeting, request.user) @@ -2668,7 +2669,7 @@ def interim_request_edit(request, number): '''Edit details of an interim meeting reqeust''' meeting = get_object_or_404(Meeting, number=number) if not can_edit_interim_request(meeting, request.user): - return HttpResponseForbidden("You do not have permissions to edit this meeting request") + permission_denied(request, "You do not have permissions to edit this meeting request") SessionFormset = inlineformset_factory( Meeting, @@ -3067,7 +3068,8 @@ def api_upload_bluesheet(request): # group: acronym or special, i.e., 'quic' or 'plenary' # item: '1', '2', '3' (the group's first, second, third etc. # session during the week) - # bluesheet: json blob with [{'name': 'Name', 'affiliation': 'Organization', }, ...] + # bluesheet: json blob with + # [{'name': 'Name', 'affiliation': 'Organization', }, ...] for item in ['meeting', 'group', 'item', 'bluesheet',]: value = request.POST.get(item) if not value: @@ -3214,9 +3216,9 @@ class ApproveSlidesForm(forms.Form): def approve_proposed_slides(request, slidesubmission_id, num): submission = get_object_or_404(SlideSubmission,pk=slidesubmission_id) if not submission.session.can_manage_materials(request.user): - return HttpResponseForbidden("You don't have permission to manage slides for this session.") + permission_denied(request, "You don't have permission to manage slides for this session.") if submission.session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"): - return HttpResponseForbidden("The materials cutoff for this session has passed. Contact the secretariat for further action.") + permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.") session_number = None sessions = get_sessions(submission.session.meeting.number,submission.session.group.acronym) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 38438184e..568671d21 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -12,7 +12,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import AnonymousUser from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.forms.models import modelformset_factory, inlineformset_factory -from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden +from django.http import Http404, HttpResponseRedirect from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse @@ -42,6 +42,7 @@ from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, from ietf.ietfauth.utils import role_required from ietf.person.models import Person from ietf.utils import log +from ietf.utils.response import permission_denied import debug # pyflakes:ignore @@ -661,20 +662,20 @@ def private_questionnaire(request, year): def process_nomination_status(request, year, nominee_position_id, state, date, hash): valid = get_hash_nominee_position(date, nominee_position_id) == hash if not valid: - return HttpResponseForbidden("Bad hash!") + permission_denied(request, "Bad hash!") expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None) if expiration_days: request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:])) if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_NOMINATION_LINK)): - return HttpResponseForbidden("Link expired") + permission_denied(request, "Link expired.") need_confirmation = True nomcom = get_nomcom_by_year(year) if nomcom.group.state_id == 'conclude': - return HttpResponseForbidden("This nomcom is concluded.") + permission_denied(request, "This nomcom is concluded.") nominee_position = get_object_or_404(NomineePosition, id=nominee_position_id) if nominee_position.state.slug != "pending": - return HttpResponseForbidden("The nomination already was %s" % nominee_position.state) + permission_denied(request, "The nomination already was %s" % nominee_position.state) state = get_object_or_404(NomineePositionStateName, slug=state) messages.info(request, "Click on 'Save' to set the state of your nomination to %s to %s (this is not a final commitment - you can notify us later if you need to change this)." % (nominee_position.position.name, state.name)) @@ -791,7 +792,7 @@ def view_feedback(request, year): def view_feedback_pending(request, year): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id == 'conclude': - return HttpResponseForbidden("This nomcom is concluded.") + permission_denied(request, "This nomcom is concluded.") extra_ids = None FeedbackFormSet = modelformset_factory(Feedback, form=PendingFeedbackForm, @@ -987,7 +988,7 @@ def edit_nomcom(request, year): if request.method == 'POST': if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') formset = ReminderDateInlineFormSet(request.POST, instance=nomcom) form = EditNomcomForm(request.POST, @@ -1069,7 +1070,7 @@ def list_positions(request, year): def remove_position(request, year, position_id): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') try: position = nomcom.position_set.get(id=position_id) except Position.DoesNotExist: @@ -1091,7 +1092,7 @@ def edit_position(request, year, position_id=None): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') if position_id: try: @@ -1136,7 +1137,7 @@ def list_topics(request, year): def remove_topic(request, year, topic_id): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') try: topic = nomcom.topic_set.get(id=topic_id) except Topic.DoesNotExist: @@ -1158,7 +1159,7 @@ def edit_topic(request, year, topic_id=None): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') if topic_id: try: @@ -1194,7 +1195,7 @@ def edit_members(request, year): nomcom = get_nomcom_by_year(year) if nomcom.group.state_id=='conclude': - return HttpResponseForbidden('This nomcom is closed.') + permission_denied(request, 'This nomcom is closed.') old_members_email = [r.email for r in nomcom.group.role_set.filter(name='member')] diff --git a/ietf/person/models.py b/ietf/person/models.py index c7aad0ed2..a197d5000 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -351,6 +351,7 @@ PERSON_API_KEY_VALUES = [ ("/api/v2/person/person", "/api/v2/person/person", "Secretariat"), ("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"), ("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"), + ("/api/notify/meeting/bluesheet", "/api/notify/meeting/bluesheet", "Recording Manager"), ] PERSON_API_KEY_ENDPOINTS = [ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES ] diff --git a/ietf/secr/announcement/views.py b/ietf/secr/announcement/views.py index a75c32731..42de089c5 100644 --- a/ietf/secr/announcement/views.py +++ b/ietf/secr/announcement/views.py @@ -1,6 +1,8 @@ +# Copyright The IETF Trust 2013-2020, All Rights Reserved + + from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpResponseForbidden from django.shortcuts import render, redirect from ietf.group.models import Role @@ -9,6 +11,8 @@ from ietf.ietfauth.utils import has_role from ietf.secr.announcement.forms import AnnounceForm from ietf.secr.utils.decorators import check_for_cancel from ietf.utils.mail import send_mail_text +from ietf.utils.response import permission_denied + # ------------------------------------------------- # Helper Functions @@ -49,7 +53,7 @@ def main(request): and send. ''' if not check_access(request.user): - return HttpResponseForbidden('Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, ISOC, NomCom.') + permission_denied(request, 'Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, ISOC, NomCom.') form = AnnounceForm(request.POST or None,user=request.user) @@ -74,7 +78,7 @@ def main(request): def confirm(request): if not check_access(request.user): - return HttpResponseForbidden('Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, ISOC, NomCom.') + permission_denied(request, 'Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, ISOC, NomCom.') if request.method == 'POST': form = AnnounceForm(request.POST, user=request.user) diff --git a/ietf/secr/utils/decorators.py b/ietf/secr/utils/decorators.py index 450a0cb56..1fa4075bd 100644 --- a/ietf/secr/utils/decorators.py +++ b/ietf/secr/utils/decorators.py @@ -1,9 +1,10 @@ +# Copyright The IETF Trust 2013-2020, All Rights Reserved from functools import wraps from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http import HttpResponseRedirect from django.shortcuts import render, get_object_or_404 from django.utils.http import urlquote @@ -12,6 +13,7 @@ from ietf.doc.models import Document from ietf.group.models import Group, Role from ietf.meeting.models import Session from ietf.secr.utils.meeting import get_timeslot +from ietf.utils.response import permission_denied def check_for_cancel(redirect_url): @@ -61,7 +63,7 @@ def check_permissions(func): try: login = request.user.person except ObjectDoesNotExist: - return HttpResponseForbidden("User not authorized to access group: %s" % group.acronym) + permission_denied(request, "User not authorized to access group: %s" % group.acronym) groups = [group] if group.parent: @@ -78,7 +80,7 @@ def check_permissions(func): return func(request, *args, **kwargs) # if we get here access is denied - return HttpResponseForbidden("User not authorized to access group: %s" % group.acronym) + permission_denied(request, "User not authorized to access group: %s" % group.acronym) return wraps(func)(wrapper) diff --git a/ietf/stats/views.py b/ietf/stats/views.py index 6e0e65bf5..54b1621e3 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -15,7 +15,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.cache import cache from django.db.models import Count, Q -from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse as urlreverse from django.utils.safestring import mark_safe @@ -39,6 +39,7 @@ from ietf.stats.models import MeetingRegistration, CountryAlias from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries, compute_hirsch_index from ietf.ietfauth.utils import has_role from ietf.utils.log import log +from ietf.utils.response import permission_denied def stats_index(request): return render(request, "stats/index.html") @@ -1080,7 +1081,7 @@ def review_stats(request, stats_type=None, acronym=None): reviewer_only_access.add(r.group_id) if not secr_access and not reviewer_only_access: - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page") teams = [t for t in teams if t.pk in secr_access or t.pk in reviewer_only_access] diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 7759c3f93..83f6b3283 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -43,6 +43,7 @@ from ietf.stats.utils import clean_country_name from ietf.utils.accesstoken import generate_access_token from ietf.utils.log import log from ietf.utils.mail import parseaddr, send_mail_message +from ietf.utils.response import permission_denied def upload_submission(request): if request.method == 'POST': @@ -298,7 +299,7 @@ def submission_status(request, submission_id, access_token=None): action = request.POST.get('action') if action == "autopost" and submission.state_id == "uploaded": if not can_edit: - return HttpResponseForbidden("You do not have permission to perform this action") + permission_denied(request, "You do not have permission to perform this action") submitter_form = SubmitterForm(request.POST, prefix="submitter") replaces_form = ReplacesForm(request.POST, name=submission.name) @@ -313,7 +314,7 @@ def submission_status(request, submission_id, access_token=None): if approvals_received: if not is_secretariat: - return HttpResponseForbidden('You do not have permission to perform this action') + permission_denied(request, 'You do not have permission to perform this action') # go directly to posting submission docevent_from_submission(request, submission, desc="Uploaded new revision") @@ -362,7 +363,7 @@ def submission_status(request, submission_id, access_token=None): elif action == "cancel" and submission.state.next_states.filter(slug="cancel"): if not can_cancel: - return HttpResponseForbidden('You do not have permission to perform this action') + permission_denied(request, 'You do not have permission to perform this action.') cancel_submission(submission) @@ -373,7 +374,7 @@ def submission_status(request, submission_id, access_token=None): elif action == "approve" and submission.state_id == "grp-appr": if not can_group_approve: - return HttpResponseForbidden('You do not have permission to perform this action') + permission_denied(request, 'You do not have permission to perform this action.') post_submission(request, submission, "WG -00 approved", "Approved and posted submission") @@ -382,7 +383,7 @@ def submission_status(request, submission_id, access_token=None): elif action == "forcepost" and submission.state.next_states.filter(slug="posted"): if not can_force_post: - return HttpResponseForbidden('You do not have permission to perform this action') + permission_denied(request, 'You do not have permission to perform this action.') if submission.state_id == "manual": desc = "Posted submission manually" @@ -424,7 +425,7 @@ def edit_submission(request, submission_id, access_token=None): submission = get_object_or_404(Submission, pk=submission_id, state="uploaded") if not can_edit_submission(request.user, submission, access_token): - return HttpResponseForbidden('You do not have permission to access this page') + permission_denied(request, 'You do not have permission to access this page.') errors = validate_submission(submission) form_errors = False @@ -638,7 +639,7 @@ def cancel_waiting_for_draft(request): can_cancel = has_role(request.user, "Secretariat") if not can_cancel: - return HttpResponseForbidden('You do not have permission to perform this action') + permission_denied(request, 'You do not have permission to perform this action.') submission_id = request.POST.get('submission_id', '') access_token = request.POST.get('access_token', '') diff --git a/ietf/sync/views.py b/ietf/sync/views.py index 9a5b797be..d6fc505c2 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -7,11 +7,11 @@ import subprocess import os import json -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404 -from django.shortcuts import render from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from ietf.doc.models import DeletedEvent, StateDocEvent, DocEvent @@ -19,6 +19,8 @@ from ietf.ietfauth.utils import role_required, has_role from ietf.sync.discrepancies import find_discrepancies from ietf.utils.serialize import object_as_shallow_dict from ietf.utils.log import log +from ietf.utils.response import permission_denied + SYNC_BIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../bin")) @@ -51,7 +53,7 @@ def notify(request, org, notification): if username != None and password != None: if settings.SERVER_MODE == "production" and not request.is_secure(): - return HttpResponseForbidden("You must use HTTPS when sending username/password") + permission_denied(request, "You must use HTTPS when sending username/password.") if not user.is_authenticated: try: @@ -63,7 +65,7 @@ def notify(request, org, notification): return HttpResponse("Invalid username/password") if not has_role(user, ("Secretariat", known_orgs[org])): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") + permission_denied(request, "You do not have the necessary permissions to view this page.") known_notifications = { "protocols": "an added reference to an RFC at the IANA protocols page" % settings.IANA_SYNC_PROTOCOLS_URL, diff --git a/ietf/templates/403.html b/ietf/templates/403.html index 8acf070f1..e117dbf17 100644 --- a/ietf/templates/403.html +++ b/ietf/templates/403.html @@ -9,9 +9,6 @@

Restricted Access.

- -

The page you tried to reach is not generally avaiable.

-

{{ exception }}

If you think this is a server error, please contact {{ bugreport_email }}.

diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index 9030af073..694e989f8 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -55,7 +55,7 @@ def require_api_key(f, request, *args, **kwargs): # Check hash key = PersonalApiKey.validate_key(force_bytes(hash)) if not key: - return err(400, "Invalid apikey") + return err(403, "Invalid apikey") # Check endpoint urlpath = request.META.get('PATH_INFO') if not (urlpath and urlpath == key.endpoint): diff --git a/ietf/utils/response.py b/ietf/utils/response.py index e6b1ed2a2..3321e99fe 100644 --- a/ietf/utils/response.py +++ b/ietf/utils/response.py @@ -6,5 +6,6 @@ from django.utils.safestring import mark_safe def permission_denied(request, msg): "A wrapper around the PermissionDenied exception" - msg += "
You can Log in if you have that role but aren't logged in." % request.path + if not request.user.is_authenticated: + msg += "
You may want to Log in if you have a datatracker role that lets you access this page." % request.path raise PermissionDenied(mark_safe(msg))