diff --git a/config/settings.py b/config/settings.py index c9d486a5..ff4ced9a 100755 --- a/config/settings.py +++ b/config/settings.py @@ -67,6 +67,8 @@ INSTALLED_APPS += [ "health_check.db", "health_check.contrib.celery", "imagekit", + # Allows authentication for Mailman + "oauth2_provider", # Allauth dependencies: "allauth", "allauth.account", @@ -107,6 +109,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "oauth2_provider.middleware.OAuth2TokenMiddleware", ] if DEBUG: @@ -286,7 +289,10 @@ HAYSTACK_CONNECTIONS = { DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -AUTHENTICATION_BACKENDS = ("allauth.account.auth_backends.AuthenticationBackend",) +AUTHENTICATION_BACKENDS = ( + "allauth.account.auth_backends.AuthenticationBackend", + "oauth2_provider.backends.OAuth2Backend", +) # GitHub settings @@ -472,3 +478,8 @@ BOOST_CALENDAR = "5rorfm42nvmpt77ac0vult9iig@group.calendar.google.com" CALENDAR_API_KEY = env("CALENDAR_API_KEY", default="changeme") EVENTS_CACHE_KEY = "homepage_events" EVENTS_CACHE_TIMEOUT = 300 # 5 min + +# OAuth settings +OAUTH_APP_NAME = ( + "Boost OAuth Concept" # Stored in the admin; replicated for convenience +) diff --git a/config/urls.py b/config/urls.py index 1701e8c0..9c417009 100755 --- a/config/urls.py +++ b/config/urls.py @@ -75,6 +75,7 @@ urlpatterns = ( path("", HomepageView.as_view(), name="home"), path("homepage-beta/", HomepageBetaView.as_view(), name="home-beta"), path("admin/", admin.site.urls), + path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("feed/downloads.rss", RSSVersionFeed(), name="downloads_feed_rss"), path("feed/downloads.atom", AtomVersionFeed(), name="downloads_feed_atom"), path("feed/news.rss", RSSNewsFeed(), name="news_feed_rss"), diff --git a/core/oauthhelper.py b/core/oauthhelper.py new file mode 100644 index 00000000..2c6fa248 --- /dev/null +++ b/core/oauthhelper.py @@ -0,0 +1,10 @@ +from django.conf import settings + +from oauth2_provider.models import Application + + +def get_oauth_client(): + try: + return Application.objects.get(name=settings.OAUTH_APP_NAME) + except Application.DoesNotExist: + return diff --git a/core/views.py b/core/views.py index 527db983..ab9424ff 100644 --- a/core/views.py +++ b/core/views.py @@ -13,6 +13,7 @@ from django.template.loader import render_to_string from django.views import View from django.views.generic import TemplateView + from .asciidoc import process_adoc_to_html_content from .boostrenderer import get_content_from_s3 from .htmlhelper import modernize_legacy_page diff --git a/docs/README.md b/docs/README.md index 19fc9234..600ca72d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # Documentation for the Boost Website - [API Documentation](./api.md) - We don't have many API endpoints, but the ones we do have are documented here +- [Boost Mailing List](./mailing_list.md) -Includes OAuth instructions - [Caching and the `RenderedContent` model](./caching_rendered_content.md) - [Dependency Management](./dependencies.md) - [Development Setup Notes](./development_setup_notes.md) diff --git a/docs/mailing_list.md b/docs/mailing_list.md new file mode 100644 index 00000000..f9f1a1f1 --- /dev/null +++ b/docs/mailing_list.md @@ -0,0 +1,18 @@ +# Boost Mailing List + +Access to the mailing list is granted to Boost users via OAuth using `django-oauth-toolkit`. + +## Setting Up Boost as an OAuth identity provider + +1. In the admin `/admin/`, create a new **Application** under the **Django OAuth Toolkit** heading. + +Screenshot 2024-01-23 at 1 34 44 PM + +2. Fill in the form: +Screenshot 2024-01-23 at 1 35 40 PM + + - The `client_id` and `client_secret` will fill in automatically. Make sure to copy and paste them somewhere before you save. + - Add the `redirect_uri` (this URL will be from the project that hosts the mailing list, and is not in **this** project. A sample URL for testing is `https://www.getpostman.com/oauth2/callback`.) + - Whatever is in the `name` field will be what the user sees on the authorization screen. + - Select **confidential** as the client type, and **Authorization code** as the grant type. + - Save the record. diff --git a/requirements.in b/requirements.in index 7190ecf1..e757ed29 100755 --- a/requirements.in +++ b/requirements.in @@ -8,6 +8,7 @@ django-db-geventpool django-extensions django-health-check django-imagekit +django-oauth-toolkit django-redis django-rest-auth django-widget-tweaks diff --git a/requirements.txt b/requirements.txt index b5e5044e..ed74faee 100755 --- a/requirements.txt +++ b/requirements.txt @@ -2,14 +2,12 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile +# pip-compile --output-file=./requirements.txt ./requirements.in # amqp==5.1.1 # via kombu appdirs==1.4.4 # via fs -appnope==0.1.3 - # via ipython asgiref==3.7.2 # via django asttokens==2.2.1 @@ -19,14 +17,14 @@ async-timeout==4.0.2 backcall==0.2.0 # via ipython beautifulsoup4==4.12.2 - # via -r requirements.in + # via -r ./requirements.in billiard==3.6.4.0 # via celery black==23.3.0 - # via -r requirements.in + # via -r ./requirements.in boto3==1.26.154 # via - # -r requirements.in + # -r ./requirements.in # django-bakery botocore==1.29.154 # via @@ -37,9 +35,9 @@ build==0.10.0 bump2version==1.0.1 # via bumpversion bumpversion==0.6.0 - # via -r requirements.in + # via -r ./requirements.in celery==5.2.7 - # via -r requirements.in + # via -r ./requirements.in certifi==2023.5.7 # via # elasticsearch @@ -70,12 +68,16 @@ coverage[toml]==7.2.7 # via pytest-cov cryptography==41.0.1 # via - # -r requirements.in + # -r ./requirements.in + # django-anymail + # jwcrypto # pyjwt decorator==5.1.1 # via ipython defusedxml==0.7.1 # via python3-openid +deprecated==1.2.14 + # via jwcrypto distlib==0.3.6 # via virtualenv dj-database-url==2.0.0 @@ -84,7 +86,7 @@ dj-email-url==1.0.6 # via environs django==4.2.2 # via - # -r requirements.in + # -r ./requirements.in # dj-database-url # django-allauth # django-anymail @@ -95,65 +97,68 @@ django==4.2.2 # django-haystack # django-health-check # django-js-asset + # django-oauth-toolkit # django-redis # django-rest-auth # django-storages # djangorestframework # model-bakery django-admin-env-notice==1.0 - # via -r requirements.in + # via -r ./requirements.in django-allauth==0.53.1 - # via -r requirements.in + # via -r ./requirements.in django-anymail[mailgun]==10.0 - # via -r requirements.in + # via -r ./requirements.in django-appconf==1.0.6 # via django-imagekit django-bakery==0.13.2 - # via -r requirements.in + # via -r ./requirements.in django-cache-url==3.4.4 # via environs django-click==2.3.0 - # via -r requirements.in + # via -r ./requirements.in django-cors-headers==4.0.0 - # via -r requirements.in + # via -r ./requirements.in django-db-geventpool==4.0.1 - # via -r requirements.in + # via -r ./requirements.in django-extensions==3.2.3 - # via -r requirements.in + # via -r ./requirements.in django-haystack==3.2.1 - # via -r requirements.in + # via -r ./requirements.in django-health-check==3.17.0 - # via -r requirements.in + # via -r ./requirements.in django-imagekit==5.0.0 - # via -r requirements.in + # via -r ./requirements.in django-js-asset==2.0.0 # via django-mptt django-mptt==0.14 - # via -r requirements.in + # via -r ./requirements.in +django-oauth-toolkit==2.3.0 + # via -r ./requirements.in django-redis==5.3.0 - # via -r requirements.in + # via -r ./requirements.in django-rest-auth==0.9.5 - # via -r requirements.in + # via -r ./requirements.in django-storages==1.13.2 - # via -r requirements.in + # via -r ./requirements.in django-test-plus==2.2.1 - # via -r requirements.in + # via -r ./requirements.in django-tracer==0.9.3 - # via -r requirements.in + # via -r ./requirements.in django-widget-tweaks==1.4.12 - # via -r requirements.in + # via -r ./requirements.in djangorestframework==3.14.0 # via - # -r requirements.in + # -r ./requirements.in # django-rest-auth elasticsearch==7.17.9 - # via -r requirements.in + # via -r ./requirements.in environs[django]==9.5.0 - # via -r requirements.in + # via -r ./requirements.in executing==1.2.0 # via stack-data faker==18.10.1 - # via -r requirements.in + # via -r ./requirements.in fastcore==1.5.29 # via ghapi filelock==3.12.2 @@ -161,15 +166,15 @@ filelock==3.12.2 fs==2.4.16 # via django-bakery gevent==22.10.2 - # via -r requirements.in + # via -r ./requirements.in ghapi==1.0.4 - # via -r requirements.in + # via -r ./requirements.in greenlet==2.0.1 # via - # -r requirements.in + # -r ./requirements.in # gevent gunicorn==20.1.0 - # via -r requirements.in + # via -r ./requirements.in identify==2.5.24 # via pre-commit idna==3.4 @@ -177,13 +182,15 @@ idna==3.4 iniconfig==2.0.0 # via pytest ipython==8.14.0 - # via -r requirements.in + # via -r ./requirements.in jedi==0.18.2 # via ipython jmespath==1.0.1 # via # boto3 # botocore +jwcrypto==1.5.1 + # via django-oauth-toolkit kombu==5.3.1 # via celery marshmallow==3.19.0 @@ -191,17 +198,19 @@ marshmallow==3.19.0 matplotlib-inline==0.1.6 # via ipython minio==7.1.15 - # via -r requirements.in + # via -r ./requirements.in mistletoe==1.1.0 - # via -r requirements.in + # via -r ./requirements.in model-bakery==1.12.0 - # via -r requirements.in + # via -r ./requirements.in mypy-extensions==1.0.0 # via black nodeenv==1.8.0 # via pre-commit oauthlib==3.2.2 - # via requests-oauthlib + # via + # django-oauth-toolkit + # requests-oauthlib packaging==23.1 # via # black @@ -222,10 +231,10 @@ pilkit==3.0 # via django-imagekit pillow==9.4.0 # via - # -r requirements.in + # -r ./requirements.in # pilkit pip-tools==6.13.0 - # via -r requirements.in + # via -r ./requirements.in platformdirs==3.5.3 # via # black @@ -233,7 +242,7 @@ platformdirs==3.5.3 pluggy==1.0.0 # via pytest pre-commit==3.3.3 - # via -r requirements.in + # via -r ./requirements.in prompt-toolkit==3.0.38 # via # click-repl @@ -241,7 +250,7 @@ prompt-toolkit==3.0.38 psycogreen==1.0.2 # via django-db-geventpool psycopg2-binary==2.9.6 - # via -r requirements.in + # via -r ./requirements.in ptyprocess==0.7.0 # via pexpect pure-eval==0.2.2 @@ -256,24 +265,24 @@ pyproject-hooks==1.0.0 # via build pytest==7.3.2 # via - # -r requirements.in + # -r ./requirements.in # pytest-cov # pytest-django pytest-cov==4.1.0 - # via -r requirements.in + # via -r ./requirements.in pytest-django==4.5.2 - # via -r requirements.in + # via -r ./requirements.in python-dateutil==2.8.2 # via - # -r requirements.in + # -r ./requirements.in # botocore # faker python-dotenv==1.0.0 # via environs python-frontmatter==1.0.0 - # via -r requirements.in + # via -r ./requirements.in python-json-logger==2.0.7 - # via -r requirements.in + # via -r ./requirements.in python3-openid==3.2.0 # via django-allauth pytz==2023.3 @@ -287,23 +296,25 @@ pyyaml==6.0 # responses redis==4.5.4 # via - # -r requirements.in + # -r ./requirements.in # django-redis requests==2.31.0 # via - # -r requirements.in + # -r ./requirements.in # django-allauth # django-anymail + # django-oauth-toolkit # requests-oauthlib # responses requests-oauthlib==1.3.1 # via django-allauth responses==0.23.1 - # via -r requirements.in + # via -r ./requirements.in s3transfer==0.6.1 # via boto3 six==1.16.0 # via + # asttokens # django-bakery # django-rest-auth # fs @@ -315,7 +326,7 @@ sqlparse==0.4.4 stack-data==0.6.2 # via ipython structlog==23.1.0 - # via -r requirements.in + # via -r ./requirements.in traitlets==5.9.0 # via # ipython @@ -334,6 +345,7 @@ urllib3==1.26.16 # responses vine==5.0.0 # via + # amqp # celery # kombu virtualenv==20.23.0 @@ -342,10 +354,12 @@ wcwidth==0.2.6 # via prompt-toolkit wheel==0.40.0 # via - # -r requirements.in + # -r ./requirements.in # pip-tools whitenoise==6.5.0 - # via -r requirements.in + # via -r ./requirements.in +wrapt==1.16.0 + # via deprecated zope-event==4.6 # via gevent zope-interface==6.0 diff --git a/users/tasks.py b/users/tasks.py index 85e12150..0adb9416 100644 --- a/users/tasks.py +++ b/users/tasks.py @@ -2,6 +2,9 @@ import structlog from django.contrib.auth import get_user_model +from celery import shared_task +from oauth2_provider.models import clear_expired + from config.celery import app from core.githubhelper import GithubAPIClient @@ -31,3 +34,12 @@ def update_user_github_photo(user_pk): avatar_url = response["avatar_url"] user.save_image_from_github(avatar_url) logger.info("users_tasks_update_gh_photo_finished", user_pk=user_pk) + + +# OAuth2 Tasks + + +@shared_task +def clear_tokens(): + """Clears all expired tokens""" + clear_expired()