* chore: Remove unused "rendertest" stuff (#6015) * fix: restore ability to create status change documents (#5963) * fix: restore ability to create status change documents Fixes #5962 * chore: address review comment * fix: Provide human-friendly status in submission status API response (#6011) Co-authored-by: nectostr <bastinda96@gmail.com> * fix: Make name/email lookups case-insensitive (#5972) (#6007) * fix: Make name/email lookups case-insensitive (#5972) Use icontains so that looking up name or email is case insensitive Added a test Fixes: 5972 * fix: Use __iexact not __icontains * fix: Clarify no-action-needed (#5918) (#6020) When a draft is submitted for manual processing, clarify that no action is needed; the Secretariat has the next steps. Fixes: #5918 * fix: Fix menu hover issue (#6019) * fix: Fix menu hover issue Fixes #5702 * Fix leftmenu hover issue * fix: Server error from api_get_session_materials() (#6025) Fixes #5877 * fix: Clarify Questionnaire label (#4688) (#6017) When filtering nominees, `Questionnaire` implies `Accepted == yes` so fix the dropdown test tosay that. Fixes: #4688 * chore: Merge from @martinthomson's rfc-txt-html (#6023) * fix:no history entry when changing RFC Editor note for doc (#6021) * fix:no history entry when changing RFC Editor note for doc * fix:no history entry when changing RFC Editor note for doc --------- Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> * fix: avoid deprecation warning on view_list() for objs without CommunityList Fixes #5942 * fix: return 404 for non-existing revisions (#6014) * fix: return 404 for non-existing revisions Links to non-existing revisions to docs should return 404 * fix: change rfc/rev and search behaviour * refactor: fix tab level * fix: return 404 for rfc revision for bibtex * fix: provide date for revisions in bibtex output (#6029) * fix: provide date for revisions in bibtex output * refactor: change walrus to if's * fix: specify particular revision for events * fix: review refactoring issue fixes #5447 * fix: Remove automatically suggested document for document that is already has review request (fixes #3211) (#5425) * Added check that if there is already review request for the document in question, ignore the automatic suggestion for that document. Fixes #3211. * fix: dont block on open requests for a previous version. Add tests --------- Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * feat: IAB statements (#5940) * feat: support iab and iesg statements. Import iab statements. (#5895) * feat: infrastructure for statements doctype * chore: basic test framework * feat: basic statement document view * feat: show replaced statements * chore: black * fix: state help for statements * fix: cleanout non-relevant email expansions * feat: import iab statements, provide group statements tab * fix: guard against running import twice * feat: build redirect csv for iab statements * fix: set document state on import * feat: show published date on main doc view * feat: handle pdf statements * feat: create new and update statements * chore: copyright block updates * chore: remove flakes * chore: black * feat: add edit/new buttons for the secretariat * fix: address PR #5895 review comments * fix: pin pydantic until inflect catches up (#5901) (#5902) * chore: re-un-pin pydantic * feat: include submitter in email about submitted slides (#6033) * feat: include submitter in email about submitted slides fixes #6031 * chore: remove unintended whitespace change * chore(dev): update .vscode/settings.json with new taskExplorer settings * fix: Add editorial stream to proceedings (#6027) * fix: Add editorial stream to proceedings Fixes #5717 * fix: Move editorial stream after the irtf in proceedings * fix: Add editorial stream to meeting materials (#6047) Fixes #6042 * fix: Shows requested reviews for doc fixes (#6022) * Fix: Shows requested reviews for doc * Changed template includes to only give required variables to them. * feat: allow openId to choose an unactive email if there are none active (#6041) * feat: allow openId to choose an unactive email if there are no active ones * chore: correct typo * chore: rename unactive to inactive * fix: Make review table more responsive (#6053) * fix: Improve layout of review table * Progress * Progress * Final changes * Fix tests * Remove fluff * Undo commits * ci: add --validate-html-harder to tests * ci: add --validate-html-harder to build.yml workflow * fix: Set colspan to actual number of columns (#6069) * fix: Clean up view_feedback_pending (#6070) - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * docs: Update LICENSE year * fix: Remove IESG state edit button when state is 'dead' (#6051) (#6065) * fix: Correctly order "last call requested" column in the IESG dashboard (#6079) * ci: update dev sandbox init script to start memcached * feat: Reclassify nomcom feedback (#6002) * fix: Clean up view_feedback_pending - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * feat: Reclassify nomcom feedback (#4669) - There's a new `Chair/Advisor Tasks` menu item `Reclassify feedback`. - I overloaded `view_feedback*` URLs with a `?reclassify` parameter. - This adds a checkbox to each feedback message, and a `Reclassify` button at the bottom of each feedback page. - "Reclassifying" basically de-classifies the feedback, and punts it back to the "Pending emails" view for reclassification. - If a feedback has been applied to multiple nominees, declassifying it from one nominee removes it from all. * fix: Remove unused local variables * fix: Fix some missing and mis-nested html * test: Add tests for reclassifying feedback * refactor: Substantial redesign of feedback reclassification - Break out reclassify_feedback* as their own URLs and views, and revert changes to view_feedback*.html. - Replace checkboxes with a Reclassify button on each message. * fix: Remember to clear the feedback associations when reclassifying * feat: Add an 'Overcome by events' feedback type * refactor: When invoking reclassification from a view-feedback page, load the corresponding reclassify-feedback page * fix: De-conflict migration with 0004_statements Also change the coding style to match, and add a reverse migration. * fix: Fix a test case to account for new feedback type * fix: 842e730 broke the Back button * refactor: Reclassify feedback directly instead of putting it back in the work queue * fix: Adjust tests to new workflow * refactor: Further refine reclassification to avoid redirects * refactor: Impose a FeedbackTypeName ordering Also add FeedbackTypeName.legend field, rather than synthesizing it every time we classify or reclassify feedback. In the reclassification forms, only show the relevant feedback types. * refactor: Merge reclassify_feedback_* back into view_feedback_* This means the "Reclassify" button is always present, but eliminates some complexity. * refactor: Add filter(used=True) on FeedbackTypeName querysets * refactor: Add the new FeedbackTypeName to the reclassification success message * fix: Secure reclassification against rogue nomcom members * fix: Print decoded key and fully clean up test nomcom (#6094) * fix: Delete Person records when deleting a test nomcom * fix: Decode test nomcom private key before printing * test: Use correct time zone for test_statement_doc_view (#6064) * chore(deps): update all npm dependencies for playwright (#6061) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/diff (#6062) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/coverage-action (#6063) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * fix: Hash cache key for default memcached cache (#6089) * feat: Show docs that an AD hasn't balloted on that need ballots to progress (#6075) * fix(doc): Unify help texts for document states (#6060) * Fix IESG State help text link (only) * Intermediate checkpoint * Correct URL filtering of state descriptions * Unify help texts for document states * Remove redundant load static from template --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * ci: fix sandbox start.sh memcached user * fix: refactor how settings handles cache definitions (#6099) * fix: refactor how settings handles cache definitions * chore: more english-speaker readable expression * fix: Cast cache key to str before calling encode (#6100) --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> Co-authored-by: Liubov Kurafeeva <liubov.kurafeeva@gmail.com> Co-authored-by: nectostr <bastinda96@gmail.com> Co-authored-by: Rich Salz <rsalz@akamai.com> Co-authored-by: PriyankaN <priyanka@amsl.com> Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> Co-authored-by: Ali <alireza83@gmail.com> Co-authored-by: Roman Beltiukov <maybe.hello.world@gmail.com> Co-authored-by: Tero Kivinen <kivinen@iki.fi> Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Kesara Rathnayake <kesara@fq.nz> Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org> Co-authored-by: Paul Selkirk <paul@painless-security.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Jim Fenton <fenton@bluepopcorn.net>
325 lines
14 KiB
Python
325 lines
14 KiB
Python
# Copyright The IETF Trust 2013-2022, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# various authentication and authorization utilities
|
|
|
|
import oidc_provider.lib.claims
|
|
|
|
|
|
from functools import wraps, WRAPPER_ASSIGNMENTS
|
|
from urllib.parse import quote as urlquote
|
|
|
|
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
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
import debug # pyflakes:ignore
|
|
|
|
from ietf.group.models import Role, GroupFeatures
|
|
from ietf.person.models import Person
|
|
from ietf.person.utils import get_dots
|
|
from ietf.doc.utils_bofreq import bofreq_editors
|
|
|
|
def user_is_person(user, person):
|
|
"""Test whether user is associated with person."""
|
|
if not user.is_authenticated or not person:
|
|
return False
|
|
|
|
if person.user_id == None:
|
|
return False
|
|
|
|
return person.user_id == user.id
|
|
|
|
def has_role(user, role_names, *args, **kwargs):
|
|
"""Determines whether user has any of the given standard roles
|
|
given. Role names must be a list or, in case of a single value, a
|
|
string."""
|
|
if not isinstance(role_names, (list, tuple)):
|
|
role_names = [ role_names ]
|
|
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
# use cache to avoid checking the same permissions again and again
|
|
if not hasattr(user, "roles_check_cache"):
|
|
user.roles_check_cache = {}
|
|
|
|
key = frozenset(role_names)
|
|
if key not in user.roles_check_cache:
|
|
try:
|
|
person = user.person
|
|
except Person.DoesNotExist:
|
|
return False
|
|
|
|
role_qs = {
|
|
"Area Director": Q(person=person, name__in=("pre-ad", "ad"), group__type="area", group__state="active"),
|
|
"Secretariat": Q(person=person, name="secr", group__acronym="secretariat"),
|
|
"IAB" : Q(person=person, name="member", group__acronym="iab"),
|
|
"IANA": Q(person=person, name="auth", group__acronym="iana"),
|
|
"RFC Editor": Q(person=person, name="auth", group__acronym="rpc"),
|
|
"ISE" : Q(person=person, name="chair", group__acronym="ise"),
|
|
"IAD": Q(person=person, name="admdir", group__acronym="ietf"),
|
|
"IETF Chair": Q(person=person, name="chair", group__acronym="ietf"),
|
|
"IETF Trust Chair": Q(person=person, name="chair", group__acronym="ietf-trust"),
|
|
"IRTF Chair": Q(person=person, name="chair", group__acronym="irtf"),
|
|
"RSAB Chair": Q(person=person, name="chair", group__acronym="rsab"),
|
|
"IAB Chair": Q(person=person, name="chair", group__acronym="iab"),
|
|
"IAB Executive Director": Q(person=person, name="execdir", group__acronym="iab"),
|
|
"IAB Group Chair": Q(person=person, name="chair", group__type="iab", group__state="active"),
|
|
"IAOC Chair": Q(person=person, name="chair", group__acronym="iaoc"),
|
|
"WG Chair": Q(person=person,name="chair", group__type="wg", group__state__in=["active","bof", "proposed"]),
|
|
"WG Secretary": Q(person=person,name="secr", group__type="wg", group__state__in=["active","bof", "proposed"]),
|
|
"RG Chair": Q(person=person,name="chair", group__type="rg", group__state__in=["active","proposed"]),
|
|
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]),
|
|
"AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]),
|
|
"RAG Secretary": Q(person=person,name="secr", group__type="rag", group__state__in=["active"]),
|
|
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
|
|
"Program Lead": Q(person=person,name="lead", group__type="program", group__state="active"),
|
|
"Program Secretary": Q(person=person,name="secr", group__type="program", group__state="active"),
|
|
"Program Chair": Q(person=person,name="chair", group__type="program", group__state="active"),
|
|
"EDWG Chair": Q(person=person, name="chair", group__type="edwg", group__state="active"),
|
|
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
|
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
|
"Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
|
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
|
|
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
|
|
"Recording Manager": Q(person=person,name="recman",group__type="ietf",group__state="active", ),
|
|
"Reviewer": Q(person=person, name="reviewer", group__state="active"),
|
|
"Review Team Secretary": Q(person=person, name="secr", group__reviewteamsettings__isnull=False,group__state="active", ),
|
|
"IRSG Member": (Q(person=person, name="member", group__acronym="irsg") | Q(person=person, name="chair", group__acronym="irtf") | Q(person=person, name="atlarge", group__acronym="irsg")),
|
|
"RSAB Member": Q(person=person, name="member", group__acronym="rsab"),
|
|
"Robot": Q(person=person, name="robot", group__acronym="secretariat"),
|
|
}
|
|
|
|
filter_expr = Q(pk__in=[]) # ensure empty set is returned if no other terms are added
|
|
for r in role_names:
|
|
filter_expr |= role_qs[r]
|
|
|
|
user.roles_check_cache[key] = bool(Role.objects.filter(filter_expr).exists())
|
|
|
|
return user.roles_check_cache[key]
|
|
|
|
|
|
# convenient decorator
|
|
|
|
def passes_test_decorator(test_func, message):
|
|
"""Decorator creator that creates a decorator for checking that
|
|
user passes the test, redirecting to login or returning a 403
|
|
error. The test function should be on the form fn(user) ->
|
|
true/false."""
|
|
def decorate(view_func):
|
|
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
|
|
def inner(request, *args, **kwargs):
|
|
if not request.user.is_authenticated:
|
|
return HttpResponseRedirect('%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, urlquote(request.get_full_path())))
|
|
elif test_func(request.user, *args, **kwargs):
|
|
return view_func(request, *args, **kwargs)
|
|
else:
|
|
raise PermissionDenied(message)
|
|
return inner
|
|
return decorate
|
|
|
|
|
|
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)))
|
|
|
|
# specific permissions
|
|
|
|
def is_authorized_in_doc_stream(user, doc):
|
|
"""Return whether user is authorized to perform stream duties on
|
|
document."""
|
|
if has_role(user, ["Secretariat"]):
|
|
return True
|
|
|
|
if not user.is_authenticated:
|
|
return False
|
|
|
|
# must be authorized in the stream or group
|
|
|
|
if (not doc.stream or doc.stream.slug == "ietf") and has_role(user, ["Area Director"]):
|
|
return True
|
|
|
|
if not doc.stream:
|
|
return False
|
|
|
|
if doc.stream.slug == "ietf" and doc.group.type_id == "individ":
|
|
return False
|
|
|
|
docman_roles = doc.group.features.docman_roles
|
|
if doc.stream.slug == "ietf":
|
|
group_req = Q(group=doc.group)
|
|
elif doc.stream.slug == "irtf":
|
|
group_req = Q(group__acronym=doc.stream.slug) | Q(group=doc.group)
|
|
elif doc.stream.slug == "iab":
|
|
if doc.group.type.slug == 'individ' or doc.group.acronym == 'iab':
|
|
docman_roles = GroupFeatures.objects.get(type_id="iab").docman_roles
|
|
group_req = Q(group__acronym=doc.stream.slug)
|
|
elif doc.stream.slug == "ise":
|
|
if doc.group.type.slug == 'individ':
|
|
docman_roles = GroupFeatures.objects.get(type_id="ietf").docman_roles
|
|
group_req = Q(group__acronym=doc.stream.slug)
|
|
elif doc.stream.slug == "editorial":
|
|
group_req = Q(group=doc.group) | Q(group__acronym='rsab')
|
|
if doc.group.type.slug in ("individ", "edappr"):
|
|
docman_roles = GroupFeatures.objects.get(type_id="edappr").docman_roles
|
|
else:
|
|
group_req = Q() # no group constraint for other cases
|
|
|
|
return Role.objects.filter(Q(name__in=docman_roles, person__user=user) & group_req).exists()
|
|
|
|
def is_authorized_in_group(user, group):
|
|
"""Return whether user is authorized to perform duties on
|
|
a given group."""
|
|
|
|
if not user.is_authenticated:
|
|
return False
|
|
|
|
if has_role(user, ["Secretariat",]):
|
|
return True
|
|
|
|
if group.parent:
|
|
if group.parent.type_id == 'area' and has_role(user, ['Area Director',]):
|
|
return True
|
|
if group.parent.acronym == 'irtf' and has_role(user, ['IRTF Chair',]):
|
|
return True
|
|
if group.parent.acronym == 'iab' and has_role(user, ['IAB','IAB Executive Director',]):
|
|
return True
|
|
|
|
return Role.objects.filter(name__in=group.features.groupman_roles, person__user=user,group=group ).exists()
|
|
|
|
def is_individual_draft_author(user, doc):
|
|
|
|
if not user.is_authenticated:
|
|
return False
|
|
|
|
if not doc.type_id=='draft':
|
|
return False
|
|
|
|
if not doc.group.type_id == "individ" :
|
|
return False
|
|
|
|
if not hasattr(user, 'person'):
|
|
return False
|
|
|
|
if user.person in doc.authors():
|
|
return True
|
|
|
|
return False
|
|
|
|
def is_bofreq_editor(user, doc):
|
|
if not user.is_authenticated:
|
|
return False
|
|
if not doc.type_id=='bofreq':
|
|
return False
|
|
return user.person in bofreq_editors(doc)
|
|
|
|
def openid_userinfo(claims, user):
|
|
# Populate claims dict.
|
|
person = get_object_or_404(Person, user=user)
|
|
email = person.email_allowing_inactive()
|
|
if person.photo:
|
|
photo_url = person.cdn_photo_url()
|
|
else:
|
|
photo_url = ''
|
|
claims.update( {
|
|
'name': person.plain_name(),
|
|
'given_name': person.first_name(),
|
|
'family_name': person.last_name(),
|
|
'nickname': '-',
|
|
'email': email.address if email else '',
|
|
'picture': photo_url,
|
|
} )
|
|
return claims
|
|
|
|
oidc_provider.lib.claims.StandardScopeClaims.info_profile = (
|
|
'Basic profile',
|
|
'Access to your basic datatracker information: Name and photo (if present).'
|
|
)
|
|
|
|
class OidcExtraScopeClaims(oidc_provider.lib.claims.ScopeClaims):
|
|
|
|
info_roles = (
|
|
"Datatracker role information",
|
|
"Access to a list of your IETF roles as known by the datatracker"
|
|
)
|
|
|
|
def scope_roles(self):
|
|
roles = self.user.person.role_set.filter(group__state_id__in=('active','bof','proposed')).values_list('name__slug', 'group__acronym')
|
|
info = {
|
|
'roles': list(roles)
|
|
}
|
|
return info
|
|
|
|
def scope_dots(self):
|
|
dots = get_dots(self.user.person)
|
|
return { 'dots': dots }
|
|
|
|
def scope_pronouns(self):
|
|
return { 'pronouns': self.user.person.pronouns() }
|
|
|
|
info_registration = (
|
|
"IETF Meeting Registration Info",
|
|
"Access to public IETF meeting registration information for the current meeting. "
|
|
"Includes meeting number, affiliation, registration type and ticket type.",
|
|
)
|
|
|
|
def scope_registration(self):
|
|
from ietf.meeting.helpers import get_current_ietf_meeting
|
|
from ietf.stats.models import MeetingRegistration
|
|
meeting = get_current_ietf_meeting()
|
|
person = self.user.person
|
|
email_list = person.email_set.values_list('address')
|
|
q = Q(person=person, meeting=meeting) | Q(email__in=email_list, meeting=meeting)
|
|
regs = MeetingRegistration.objects.filter(q).distinct()
|
|
for reg in regs:
|
|
if not reg.person_id:
|
|
reg.person = person
|
|
reg.save()
|
|
info = {}
|
|
if regs:
|
|
# fill in info to return
|
|
ticket_types = set([])
|
|
reg_types = set([])
|
|
for reg in regs:
|
|
ticket_types.add(reg.ticket_type)
|
|
reg_types.add(reg.reg_type)
|
|
info = {
|
|
'meeting': meeting.number,
|
|
# full_week, one_day, student:
|
|
'ticket_type': ' '.join(ticket_types),
|
|
# onsite, remote, hackathon_onsite, hackathon_remote:
|
|
'reg_type': ' '.join(reg_types),
|
|
'affiliation': ([ reg.affiliation for reg in regs if reg.affiliation ] or [''])[0],
|
|
}
|
|
|
|
return info
|
|
|
|
def can_request_rfc_publication(user, doc):
|
|
"""Answers whether this user has an appropriate role to send this document to the RFC Editor for publication as an RFC.
|
|
|
|
This not take anything but the stream of the document into account.
|
|
|
|
NOTE: This intentionally always returns False for IETF stream documents.
|
|
The publication request process for the IETF stream is handled by the
|
|
secretariat at ietf.doc.views_ballot.approve_ballot"""
|
|
|
|
if doc.stream_id == "irtf":
|
|
return has_role(user, ("Secretariat", "IRTF Chair"))
|
|
elif doc.stream_id == "editorial":
|
|
return has_role(user, ("Secretariat", "RSAB Chair"))
|
|
elif doc.stream_id == "ise":
|
|
return has_role(user, ("Secretariat", "ISE"))
|
|
elif doc.stream_id == "iab":
|
|
return has_role(user, ("Secretariat", "IAB Chair"))
|
|
elif doc.stream_id == "ietf":
|
|
return False # See the docstring
|
|
else:
|
|
return False
|