diff --git a/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py b/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py new file mode 100644 index 000000000..adc0e6962 --- /dev/null +++ b/ietf/doc/migrations/0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.0.10 on 2023-05-16 20:36 + +from django.db import migrations +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0001_initial'), + ('doc', '0003_remove_document_info_order'), + ] + + operations = [ + migrations.AlterField( + model_name='dochistory', + name='ad', + field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'), + ), + migrations.AlterField( + model_name='dochistory', + name='shepherd', + field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'), + ), + migrations.AlterField( + model_name='document', + name='ad', + field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'), + ), + migrations.AlterField( + model_name='document', + name='shepherd', + field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'), + ), + ] diff --git a/ietf/settings.py b/ietf/settings.py index e0c6820a8..3fd08ffa4 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -13,6 +13,7 @@ import warnings from typing import Any, Dict, List, Tuple # pyflakes:ignore warnings.simplefilter("always", DeprecationWarning) +warnings.filterwarnings("ignore", message="'oidc_provider' defines default_app_config") # hopefully only need until Django 4.1 or 4.2 warnings.filterwarnings("ignore", message="'urllib3\\[secure\\]' extra is deprecated") warnings.filterwarnings("ignore", message="The logout\\(\\) view is superseded by") warnings.filterwarnings("ignore", message="Report.file_reporters will no longer be available in Coverage.py 4.2", module="coverage.report") @@ -100,7 +101,13 @@ SITE_ID = 1 # to load the internationalization machinery. USE_I18N = False +# Django 4.0 changed the default setting of USE_L10N to True. The setting +# is deprecated and will be removed in Django 5.0. +USE_L10N = False + USE_TZ = True +USE_DEPRECATED_PYTZ = True # supported until Django 5 + # Default primary key field type to use for models that don’t have a field with primary_key=True. # In the future (relative to 4.2), the default will become 'django.db.models.BigAutoField.' @@ -319,7 +326,14 @@ UTILS_LOGGER_LEVELS: Dict[str, str] = { X_FRAME_OPTIONS = 'SAMEORIGIN' -CSRF_TRUSTED_ORIGINS = ['ietf.org', '*.ietf.org', 'meetecho.com', '*.meetecho.com', 'gather.town', '*.gather.town', ] +CSRF_TRUSTED_ORIGINS = [ + "https://ietf.org", + "https://*.ietf.org", + 'https://meetecho.com', + 'https://*.meetecho.com', + 'https://gather.town', + 'https://*.gather.town', +] CSRF_COOKIE_SAMESITE = 'None' CSRF_COOKIE_SECURE = True diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index d37d255a3..db919bd4b 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -5,6 +5,7 @@ import datetime from decorator import decorator, decorate +from functools import wraps from django.conf import settings from django.contrib.auth import login @@ -39,52 +40,54 @@ def person_required(f, request, *args, **kwargs): return render(request, 'registration/missing_person.html') return f(request, *args, **kwargs) -@decorator -def require_api_key(f, request, *args, **kwargs): - - def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') - # Check method and get hash - if request.method == 'POST': - hash = request.POST.get('apikey') - elif request.method == 'GET': - hash = request.GET.get('apikey') - else: - return err(405, "Method not allowed") - if not hash: - return err(400, "Missing apikey parameter") - # Check hash - key = PersonalApiKey.validate_key(force_bytes(hash)) - if not key: - return err(403, "Invalid apikey") - # Check endpoint - urlpath = request.META.get('PATH_INFO') - if not (urlpath and urlpath == key.endpoint): - return err(400, "Apikey endpoint mismatch") - # Check time since regular login - person = key.person - last_login = person.user.last_login - if not person.user.is_staff: - time_limit = (timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS)) - if last_login == None or last_login < time_limit: - return err(400, "Too long since last regular login") - # Log in - login(request, person.user) - # restore the user.last_login field, so it reflects only gui logins - person.user.last_login = last_login - person.user.save() - # Update stats - key.count += 1 - key.latest = timezone.now() - key.save() - PersonApiKeyEvent.objects.create(person=person, type='apikey_login', key=key, desc="Logged in with key ID %s, endpoint %s" % (key.id, key.endpoint)) - # Execute decorated function - try: - ret = f(request, *args, **kwargs) - except AttributeError as e: - log.log("Bad API call: args: %s, kwargs: %s, exception: %s" % (args, kwargs, e)) - return err(400, "Bad or missing parameters") - return ret + +def require_api_key(f): + @wraps(f) + def _wrapper(request, *args, **kwargs): + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + # Check method and get hash + if request.method == 'POST': + hash = request.POST.get('apikey') + elif request.method == 'GET': + hash = request.GET.get('apikey') + else: + return err(405, "Method not allowed") + if not hash: + return err(400, "Missing apikey parameter") + # Check hash + key = PersonalApiKey.validate_key(force_bytes(hash)) + if not key: + return err(403, "Invalid apikey") + # Check endpoint + urlpath = request.META.get('PATH_INFO') + if not (urlpath and urlpath == key.endpoint): + return err(400, "Apikey endpoint mismatch") + # Check time since regular login + person = key.person + last_login = person.user.last_login + if not person.user.is_staff: + time_limit = (timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS)) + if last_login == None or last_login < time_limit: + return err(400, "Too long since last regular login") + # Log in + login(request, person.user) + # restore the user.last_login field, so it reflects only gui logins + person.user.last_login = last_login + person.user.save() + # Update stats + key.count += 1 + key.latest = timezone.now() + key.save() + PersonApiKeyEvent.objects.create(person=person, type='apikey_login', key=key, desc="Logged in with key ID %s, endpoint %s" % (key.id, key.endpoint)) + # Execute decorated function + try: + ret = f(request, *args, **kwargs) + except AttributeError as e: + log.log("Bad API call: args: %s, kwargs: %s, exception: %s" % (args, kwargs, e)) + return err(400, "Bad or missing parameters") + return ret + return _wrapper def _memoize(func, self, *args, **kwargs): diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index 281b22724..8905eab90 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -74,7 +74,7 @@ from django.urls import URLResolver # type: ignore from django.template.backends.django import DjangoTemplates from django.template.backends.django import Template # type: ignore[attr-defined] from django.utils import timezone -# from django.utils.safestring import mark_safe +from django.views.generic import RedirectView, TemplateView import debug # pyflakes:ignore debug.debug = True @@ -550,8 +550,10 @@ class CoverageTest(unittest.TestCase): return (regex in ("^_test500/$", "^accounts/testemail/$") or regex.startswith("^admin/") or re.search('^api/v1/[^/]+/[^/]+/', regex) - or getattr(pattern.callback, "__name__", "") == "RedirectView" - or getattr(pattern.callback, "__name__", "") == "TemplateView" + or ( + hasattr(pattern.callback, "view_class") + and issubclass(pattern.callback.view_class, (RedirectView, TemplateView)) + ) or pattern.callback == django.views.static.serve) patterns = [(regex, re.compile(regex, re.U), obj) for regex, obj in url_patterns diff --git a/ietf/utils/urls.py b/ietf/utils/urls.py index 9c26da724..6abda9b97 100644 --- a/ietf/utils/urls.py +++ b/ietf/utils/urls.py @@ -11,7 +11,9 @@ from django.utils.encoding import force_str from django.views.generic import View def url(regex, view, kwargs=None, name=None): - if callable(view) and hasattr(view, '__name__'): + if hasattr(view, "view_class"): + view_name = "%s.%s" % (view.__module__, view.view_class.__name__) + elif callable(view) and hasattr(view, '__name__'): view_name = "%s.%s" % (view.__module__, view.__name__) else: view_name = regex diff --git a/patch/django-cookie-delete-with-all-settings.patch b/patch/django-cookie-delete-with-all-settings.patch index bf4e45c2e..9b327928d 100644 --- a/patch/django-cookie-delete-with-all-settings.patch +++ b/patch/django-cookie-delete-with-all-settings.patch @@ -1,6 +1,6 @@ --- django/contrib/messages/storage/cookie.py.orig 2020-08-13 11:10:36.719177122 +0200 +++ django/contrib/messages/storage/cookie.py 2020-08-13 11:45:23.503463150 +0200 -@@ -108,6 +108,8 @@ +@@ -109,6 +109,8 @@ response.delete_cookie( self.cookie_name, domain=settings.SESSION_COOKIE_DOMAIN, @@ -11,39 +11,42 @@ --- django/http/response.py.orig 2020-08-13 11:16:04.060627793 +0200 +++ django/http/response.py 2020-08-13 11:54:03.482476973 +0200 -@@ -243,12 +243,18 @@ +@@ -261,20 +261,28 @@ value = signing.get_cookie_signer(salt=key + salt).sign(value) return self.set_cookie(key, value, **kwargs) - -- def delete_cookie(self, key, path='/', domain=None, samesite=None): -+ def delete_cookie(self, key, path='/', domain=None, secure=False, httponly=False, samesite=None): + +- def delete_cookie(self, key, path="/", domain=None, samesite=None): ++ def delete_cookie(self, key, path="/", domain=None, secure=False, httponly=False, samesite=None): # Browsers can ignore the Set-Cookie header if the cookie doesn't use # the secure flag and: # - the cookie name starts with "__Host-" or "__Secure-", or # - the samesite is "none". -- secure = ( -- key.startswith(('__Secure-', '__Host-')) or -- (samesite and samesite.lower() == 'none') +- secure = key.startswith(("__Secure-", "__Host-")) or ( +- samesite and samesite.lower() == "none" - ) + if key in self.cookies: -+ domain = self.cookies[key].get('domain', domain) -+ secure = self.cookies[key].get('secure', secure) -+ httponly = self.cookies[key].get('httponly', httponly) -+ samesite = self.cookies[key].get('samesite', samesite) ++ domain = self.cookies[key].get("domain", domain) ++ secure = self.cookies[key].get("secure", secure) ++ httponly = self.cookies[key].get("httponly", httponly) ++ samesite = self.cookies[key].get("samesite", samesite) + else: + secure = secure or ( -+ key.startswith(('__Secure-', '__Host-')) or -+ (samesite and samesite.lower() == 'none') ++ key.startswith(("__Secure-", "__Host-")) or ++ (samesite and samesite.lower() == "none") + ) self.set_cookie( -- key, max_age=0, path=path, domain=domain, secure=secure, -+ key, max_age=0, path=path, domain=domain, secure=secure, httponly=httponly, - expires='Thu, 01 Jan 1970 00:00:00 GMT', samesite=samesite, + key, + max_age=0, + path=path, + domain=domain, + secure=secure, ++ httponly=httponly, + expires="Thu, 01 Jan 1970 00:00:00 GMT", + samesite=samesite, ) - --- django/contrib/sessions/middleware.py.orig 2020-08-13 12:12:12.401898114 +0200 +++ django/contrib/sessions/middleware.py 2020-08-13 12:14:52.690520659 +0200 -@@ -40,6 +40,8 @@ +@@ -38,6 +38,8 @@ settings.SESSION_COOKIE_NAME, path=settings.SESSION_COOKIE_PATH, domain=settings.SESSION_COOKIE_DOMAIN, @@ -51,4 +54,4 @@ + httponly=settings.SESSION_COOKIE_HTTPONLY or None, samesite=settings.SESSION_COOKIE_SAMESITE, ) - patch_vary_headers(response, ('Cookie',)) + patch_vary_headers(response, ("Cookie",)) diff --git a/requirements.txt b/requirements.txt index 48491aa2a..1c39cf4ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ coverage>=4.5.4,<5.0 # Coverage 5.x moves from a json database to SQLite. Mo decorator>=5.1.1 types-decorator>=5.1.1 defusedxml>=0.7.1 # for TastyPie when using xml; not a declared dependency -Django<4 +Django<4.1 django-analytical>=3.1.0 django-bootstrap5>=21.3 django-celery-beat>=2.3.0