Skip to content

Commit

Permalink
Merge branch 'feature/sso' of github.com:torchbox/rca-wagtail-2019 in…
Browse files Browse the repository at this point in the history
…to dev
  • Loading branch information
Patrick Gan committed Nov 5, 2024
2 parents 24923f1 + 3d7b0b8 commit a3597a2
Show file tree
Hide file tree
Showing 10 changed files with 1,436 additions and 1,003 deletions.
4 changes: 4 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ You an use the endpoint below to confirm the ID works.
```python
mailchimp.lists.list_interest_category_interests(settings.MAILCHIMP_LIST_ID, MAILCHIMP_PROGRAMMES_INTEREST_CATEGORY_ID)
```

## Azure AD SSO

To add SSO functionality, we used `social-auth-app-django`. All of the configurations and necessary keys can be seen in the `Azure AD (SSO)` and `Social Auth (SSO)` sections in the base settings. Logging in via SSO is optional. Users can still login via the standard Wagtail login form.
10 changes: 10 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ As well as testing the critical paths, these areas of functionality should be ch
1. The site has custom code to show/hide fields and panels depending on if a StudentPage is viewed by an admin (superuser) or a Student (none superuser).
2. The site has custom code to show/hide sidebar elements depending on if a Student is viewing the editor vs an admin (superuser).

### Users who logged in via SSO automatically gets 'Editor' permissions.

1. When users log in via SSO, they are automatically made editors. This is done via `rca.utils.pipeline.make_sso_users_editors` and added to the `SOCIAL_AUTH_PIPELINE`. RCA will manually elevate permissiosn if necessary.

### Users who logged in via SSO is redirected to a logout confirmation when they logout.

1. The site overrides the `/admin/logout/` endpoint to redirect users who logged in to `/logout/`. This is a confirmation screen that users will still need to manually log out of their SSO accounts. This is done with `rca.account_management.views.CustomLogoutView` and `rca.account_management.views.SSOLogoutConfirmationView`.
2. Users who did not log in via SSO should be able to log out without seeing any confirmation screen.

---

## Overridden core Wagtail templates
Expand All @@ -113,6 +122,7 @@ The following templates are overridden and should be checked for changes when up
Last checked against Wagtail version: 6.1

- `rca/account_management/templates/wagtailadmin/base.html`
- `rca/project_styleguide/templates/patterns/pages/auth/login.html` - This was overridden to add the "Sign in with single sign-on" button to the login template.
- ~~`rca/users/templates/wagtailusers/users/list.html`~~ This template was deleted in 2645c204425b8fa3409a110f46b2822a1953fe49 because as of Wagtail 6.1, [it's no longer used](https://github.com/wagtail/wagtail/commit/7b1644eb37b6b6cf7800276acf9abef5254fc096). Please note that the `user_listing_buttons` template tag was used in this template, and it has since been [deprecated](https://docs.wagtail.org/en/latest/releases/6.1.html#deprecation-of-user-listing-buttons-template-tag).

!!! warning "Technical Debt - to be addressed in Wagtail 6.2"
Expand Down
2,212 changes: 1,213 additions & 999 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ sentry-sdk = "^2.15.0"
tblib = "^3.0.0"
urllib3 = "<2"
whitenoise = "~6.7"
social-auth-app-django = "^5.4.2"

[tool.poetry.extras]
gunicorn = ["gunicorn"]
Expand Down
37 changes: 35 additions & 2 deletions rca/account_management/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView
from wagtail.admin import messages
from wagtail.admin.views.account import LoginView
from wagtail.admin.views.account import LoginView, LogoutView

from rca.people.models import StudentIndexPage, StudentPage
from rca.users.models import User
Expand Down Expand Up @@ -164,7 +165,7 @@ def form_valid(self, form):
class CustomLoginView(LoginView):
"""Custom login view to redirect students to their profile page"""

template_name = "wagtailadmin/login.html"
template_name = "patterns/pages/auth/login.html"

def get_success_url(self):
"""Override the success URL if the user meets the following:
Expand All @@ -190,3 +191,35 @@ def get_success_url(self):
"wagtailadmin_pages:edit", kwargs={"page_id": student_page.id}
)
return super().get_success_url()


class CustomLogoutView(LogoutView):
"""Check if user is signed in via SSO. If yes, redirect them to a confirmation logout page."""

def dispatch(self, request, *args, **kwargs):
if (
self.request.session.get("_auth_user_backend", "")
== "social_core.backends.azuread_tenant.AzureADTenantOAuth2"
):
# redirect to `sso_logout_confirmation`
return redirect("sso_logout_confirmation")

return super().dispatch(request, *args, **kwargs)


class SSOLogoutConfirmationView(LogoutView):
template_name = "patterns/pages/auth/logout_confirmation.html"

def dispatch(self, request, *args, **kwargs):
if (
self.request.session.get("_auth_user_backend", "")
== "social_core.backends.azuread_tenant.AzureADTenantOAuth2"
):
# If the request is a POST, log out the user
if request.method == "POST":
return super().dispatch(request, *args, **kwargs)

# If the request is GET, render the confirmation page
return render(request, self.template_name)

return super().dispatch(request, *args, **kwargs)
80 changes: 80 additions & 0 deletions rca/project_styleguide/templates/patterns/pages/auth/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% extends "wagtailadmin/admin_base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% trans "Sign in" %}{% endblock %}
{% block bodyclass %}login{% endblock %}

{% block furniture %}
<main class="content-wrapper" id="main">
<h1>{% block branding_login %}{% trans "Sign in to Wagtail" %}{% endblock %}</h1>

<div class="messages" role="status">
{# Always show messages div so it can be appended to by JS #}
{% if messages or form.errors %}
<ul>
{% if form.errors %}
{% for error in form.non_field_errors %}
<li class="error">{{ error }}</li>
{% endfor %}
{% endif %}
{% for message in messages %}
<li class="{{ message.tags }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>

{% block above_login %}{% endblock %}

<form class="login-form" action="{% url 'wagtailadmin_login' %}" method="post" autocomplete="off" novalidate>
{% block login_form %}
{% csrf_token %}

{% url 'wagtailadmin_home' as home_url %}
<input type="hidden" name="next" value="{{ next|default:home_url }}" />

{% block fields %}
{% formattedfield form.username %}
{% formattedfield form.password %}

{% if show_password_reset %}
<a class="reset-password" href="{% url 'wagtailadmin_password_reset' %}">{% trans "Forgotten password?" %}</a>
{% endif %}

{% block extra_fields %}
{% for field_name, field in form.extra_fields %}
{% formattedfield field %}
{% endfor %}
{% endblock extra_fields %}

{% include "wagtailadmin/shared/forms/single_checkbox.html" with label_classname="remember-me" name="remember" text=_("Remember me") %}
{% endblock %}
{% endblock %}
<footer class="form-actions">
{% block submit_buttons %}
<button
type="submit"
class="button button-longrunning"
data-controller="w-progress"
data-action="w-progress#activate"
data-w-progress-active-value="{% trans 'Signing in…' %}"
>
{% icon name="spinner" %}
<em data-w-progress-target="label">{% trans 'Sign in' %}</em>
</button>
<br /><br />
<a class="button" href="{% url "social:begin" "azuread-tenant-oauth2" %}?next={{ request.path }}">
Sign in with single sign-on
</a>
{% endblock %}
</footer>
</form>

{% block below_login %}{% endblock %}

{% block branding_logo %}
<div class="login-logo">
{% include "wagtailadmin/logo.html" with wordmark="True" %}
</div>
{% endblock %}
</main>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% extends "wagtailadmin/admin_base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% trans "Log out" %}{% endblock %}
{% block bodyclass %}login{% endblock %}

{% block furniture %}
<main class="content-wrapper" id="main">
<h1>{% block branding_login %}{% trans "Logout from Wagtail" %}{% endblock %}</h1>

<form class="login-form" action="{% url 'sso_logout_confirmation' %}" method="post" novalidate>
{% block login_form %}
{% csrf_token %}

<div class="messages">
<p>
You have signed in using Single Sign-On (SSO). Logging out here will only end your session for this application. To completely sign out, make sure to log out from your SSO provider as well.
</p>
</div>

<button type="submit" class="button button-longrunning">
{% trans 'Log out' %}
</button>

<br /><br />

<button type="button" class="button" onclick="window.history.back();">
{% trans 'Cancel' %}
</button>
{% endblock %}
</form>

{% block branding_logo %}
<div class="login-logo">
{% include "wagtailadmin/logo.html" with wordmark="True" %}
</div>
{% endblock %}
</main>
{% endblock %}
36 changes: 36 additions & 0 deletions rca/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"wagtail_rangefilter",
"rangefilter",
"wagtail_modeladmin",
"social_django",
]

# Middleware classes
Expand Down Expand Up @@ -150,6 +151,9 @@
# This is a custom context processor that lets us add custom
# global variables to all the templates.
"rca.utils.context_processors.global_vars",
# Social auth context_processors
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
],
"builtins": ["pattern_library.loader_tags"],
},
Expand Down Expand Up @@ -858,3 +862,35 @@
)
# Vepple
VEPPLE_API_URL = env.get("VEPPLE_API_URL", "https://editor.rca.rvhosted.com")

# Azure AD (SSO)
AUTHENTICATION_BACKENDS = [
"social_core.backends.azuread_tenant.AzureADTenantOAuth2",
"django.contrib.auth.backends.ModelBackend",
]

# Social Auth (SSO)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY", None
)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET", None
)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID", None
)
SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "last_name", "email"]
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_USER_MODEL = "users.User"
SOCIAL_AUTH_PIPELINE = (
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
"social_core.pipeline.social_auth.auth_allowed",
"social_core.pipeline.social_auth.social_user",
"social_core.pipeline.social_auth.associate_by_email",
"social_core.pipeline.user.create_user",
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
"rca.utils.pipeline.make_sso_users_editors",
)
15 changes: 13 additions & 2 deletions rca/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.utils.urlpatterns import decorate_urlpatterns

from rca.account_management.views import CustomLoginView
from rca.account_management.views import (
CustomLoginView,
CustomLogoutView,
SSOLogoutConfirmationView,
)
from rca.search import views as search_views
from rca.utils.cache import get_default_cache_control_decorator
from rca.wagtailapi.api import api_router
Expand All @@ -23,6 +27,10 @@
private_urlpatterns = [
path("admin/login/", CustomLoginView.as_view(), name="wagtailcore_login"),
path("admin/_util/login/", CustomLoginView.as_view(), name="wagtailcore_login"),
path("admin/logout/", CustomLogoutView.as_view(), name="wagtailcore_logout"),
path(
"logout/", SSOLogoutConfirmationView.as_view(), name="sso_logout_confirmation"
),
path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)),
path("documents2/", include(wagtaildocs_urls)),
Expand All @@ -37,7 +45,10 @@


# Public URLs that are meant to be cached.
urlpatterns = [path("sitemap.xml", sitemap)]
urlpatterns = [
path("sitemap.xml", sitemap),
path("", include("social_django.urls", namespace="social")),
]


if settings.DEBUG:
Expand Down
6 changes: 6 additions & 0 deletions rca/utils/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib.auth.models import Group


def make_sso_users_editors(backend, user, response, *args, **kwargs):
editors = Group.objects.get(name="Editors")
user.groups.add(editors)

0 comments on commit a3597a2

Please sign in to comment.