datatracker/ietf/ietfauth/utils.py
Lars Eggert 57f23f5198
chore: feat/dark-mode <- main ()
* chore: Remove unused "rendertest" stuff ()

* fix: restore ability to create status change documents ()

* fix: restore ability to create status change documents

Fixes 

* chore: address review comment

* fix: Provide human-friendly status in submission status API response ()

Co-authored-by: nectostr <bastinda96@gmail.com>

* fix: Make name/email lookups case-insensitive () ()

* fix: Make name/email lookups case-insensitive ()

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 () ()

When a draft is submitted for manual processing, clarify that
no action is needed; the Secretariat has the next steps.

Fixes: 

* fix: Fix menu hover issue ()

* fix: Fix menu hover issue

Fixes 

* Fix leftmenu hover issue

* fix: Server error from api_get_session_materials() ()

Fixes 

* fix: Clarify Questionnaire label () ()

When filtering nominees, `Questionnaire` implies `Accepted == yes`
so fix the dropdown test tosay that.

Fixes: 

* chore: Merge from @martinthomson's rfc-txt-html ()

* fix:no history entry when changing RFC Editor note for doc ()

* 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 

* fix: return 404 for non-existing revisions ()

* 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 ()

* 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 

* fix:  Remove automatically suggested document for document that is already has review request (fixes ) ()

* Added check that if there is already review request for the document
in question, ignore the automatic suggestion for that document.
Fixes .

* 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 ()

* feat: support iab and iesg statements. Import iab statements. ()

* 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  review comments

* fix: pin pydantic until inflect catches up () ()

* chore: re-un-pin pydantic

* feat: include submitter in email about submitted slides ()

* feat: include submitter in email about submitted slides

fixes 

* chore: remove unintended whitespace change

* chore(dev): update .vscode/settings.json with new taskExplorer settings

* fix: Add editorial stream to proceedings ()

* fix: Add editorial stream to proceedings

Fixes 

* fix: Move editorial stream after the irtf in proceedings

* fix: Add editorial stream to meeting materials ()

Fixes 

* fix: Shows requested reviews for doc fixes ()

* 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 ()

* 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 ()

* 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 ()

* 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)`.

* docs: Update LICENSE year

* fix: Remove IESG state edit button when state is 'dead' () ()

* fix: Correctly order "last call requested" column in the IESG dashboard ()

* ci: update dev sandbox init script to start memcached

* feat: Reclassify nomcom feedback ()

* 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 ()

- 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 ()

* 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 ()

* chore(deps): update all npm dependencies for playwright ()

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* chore(deps): update all npm dependencies for dev/diff ()

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* chore(deps): update all npm dependencies for dev/coverage-action ()

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* fix: Hash cache key for default memcached cache ()

* feat: Show docs that an AD hasn't balloted on that need ballots to progress ()

* fix(doc): Unify help texts for document states ()

* 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 ()

* fix: refactor how settings handles cache definitions

* chore: more english-speaker readable expression

* fix: Cast cache key to str before calling encode ()

---------

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>
2023-08-10 09:49:15 -05:00

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