Skip to content

Commit

Permalink
add option to avoid sso on local (#773)
Browse files Browse the repository at this point in the history
Co-authored-by: Cameron Lamb <[email protected]>
  • Loading branch information
marcelkornblum and CamLamb authored Oct 18, 2024
1 parent 9fcfa58 commit cf6d341
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env.ci
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
DATABASE_URL=psql://postgres:postgres@db:5432/digital_workspace
DJANGO_SETTINGS_MODULE=config.settings.test
DJANGO_SECRET_KEY=test
DEV_TOOLS_ENABLED=False
SECRET_KEY=ci-secret
ALLOWED_HOSTS=*
AUTHBROKER_URL=https://test.gov.uk
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ make setup
make webpack
```

If you're on a local (non-production!) environment you may want to ensure that `DEV_TOOLS_ENABLED` is set to `True` to avoid integrating with the SSO service. This is a workaround to allow developers to impersonate different users and should be used with caution.

You can now access:

- Digital Workspace on http://localhost:8000
Expand Down
1 change: 1 addition & 0 deletions src/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def filter_transactions(event, hint):
"country_fact_sheet",
"events",
"dw_design_system",
"dev_tools",
"user.apps.UserConfig",
"pingdom.apps.PingdomConfig",
"peoplefinder.apps.PeoplefinderConfig",
Expand Down
18 changes: 18 additions & 0 deletions src/config/settings/developer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .base import * # noqa

DEBUG = True

CAN_ELEVATE_SSO_USER_PERMISSIONS = True

INSTALLED_APPS += [ # noqa F405
Expand Down Expand Up @@ -34,3 +36,19 @@
SILKY_META = True
except ModuleNotFoundError:
...


DEV_TOOLS_ENABLED = True

if DEV_TOOLS_ENABLED:
# remove Django Staff SSO Client for local login
MIDDLEWARE.remove("authbroker_client.middleware.ProtectAllViewsMiddleware")
AUTHENTICATION_BACKENDS.remove("user.backends.CustomAuthbrokerBackend") # noqa F405
# ... and add Dev Tools
DEV_TOOLS_LOGIN_URL = "dev_tools:login"
DEV_TOOLS_DEFAULT_USER = 1
# INSTALLED_APPS += ["dev_tools"]
TEMPLATES[0]["OPTIONS"]["context_processors"].append( # noqa F405
"dev_tools.context_processors.dev_tools"
)
MIDDLEWARE.append("dev_tools.middleware.DevToolsLoginRequiredMiddleware")
6 changes: 6 additions & 0 deletions src/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if hasattr(settings, "DEV_TOOLS_ENABLED") and settings.DEV_TOOLS_ENABLED:
# Dev tools purposefully only active with DEBUG=True clause
urlpatterns += [
path("dev-tools/", include("dev_tools.urls", namespace="dev_tools"))
]

urlpatterns += [
# Wagtail
path("", include(wagtail_urls)),
Expand Down
8 changes: 8 additions & 0 deletions src/core/templates/includes/footer.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{% load wagtailuserbar %}

<!-- Load dev_tools -->
{% load dev_tools %}
<!-- Add the dialog -->
{% dev_tools_dialog %}

<footer class="govuk-footer " role="contentinfo">
<div class="dwds-container">
<div class="govuk-footer__meta">
Expand Down Expand Up @@ -44,6 +49,9 @@ <h2 class="govuk-visually-hidden">Support links</h2>
<a class="govuk-footer__link"
href="mailto:[email protected]">Feedback</a>
</span>

<!-- Add a way to open the dialog -->
{% if DEV_TOOLS_ENABLED %}<button onclick="openDevToolsDialog()">Dev tools</button>{% endif %}
</div>
{% endif %}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/core/templates/includes/profile_panel.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load render_bundle webpack_static from webpack_loader %}

<div class="people-finder-panel">
{% if peoplefinder_profile %}
<a class="profile-link"
Expand Down
75 changes: 75 additions & 0 deletions src/dev_tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# dev_tools

## Installation

Add `dev_tools` to your `INSTALLED_APPS` setting.

```python
INSTALLED_APPS = [
...
"dev_tools"
]
```

Add `dev_tools.context_processors.dev_tools` to your `TEMPLATES` setting.

```python
TEMPLATES = [
...
{
...
"OPTIONS": {
"context_processors": [
...
"dev_tools.context_processors.dev_tools",
],
},
},
]
```

Add `dev_tools.middleware.DevToolsLoginRequiredMiddleware` to your `MIDDLEWARE` setting.

```python
# You should only add this middleware in dev environments where you have also set `DEV_TOOLS_ENABLED=True`.
MIDDLEWARE = [
...
"dev_tools.middleware.DevToolsLoginRequiredMiddleware",
]
```

Add `dev_tools.urls` to your `urlpatterns`.

```python
urlpatterns = [
...
path("dev-tools/", include("dev_tools.urls")),
]
```

Add the following settings.

```python
# You should disable this in production!
DEV_TOOLS_ENABLED = True
DEV_TOOLS_LOGIN_URL = None
DEV_TOOLS_DEFAULT_USER = None

# Optional - if you want to be automatically logged in as a default user.

# Use the dev_tools login view.
DEV_TOOLS_LOGIN_URL = "dev_tools:login"
# Primary key of the default user.
DEV_TOOLS_DEFAULT_USER = 1
```

Add `dev_tools_dialog` to your base template:

```html
<!-- Load dev_tools -->
{% load dev_tools %}
<!-- Add the dialog -->
{% dev_tools_dialog %}
<!-- Add a way to open the dialog -->
<button onclick="openDevToolsDialog()">Dev tools</button>
```
Empty file added src/dev_tools/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions src/dev_tools/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class DevToolsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "dev_tools"
7 changes: 7 additions & 0 deletions src/dev_tools/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.conf import settings


def dev_tools(request):
return {
"DEV_TOOLS_ENABLED": settings.DEV_TOOLS_ENABLED,
}
16 changes: 16 additions & 0 deletions src/dev_tools/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django import forms
from django.contrib.auth import get_user_model


User = get_user_model()


def get_user_choices():
return [
(None, "AnonymousUser"),
*[(x.id, str(x)) for x in User.objects.all()],
]


class ChangeUserForm(forms.Form):
user = forms.ChoiceField(choices=get_user_choices, required=False)
29 changes: 29 additions & 0 deletions src/dev_tools/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.conf import settings
from django.shortcuts import redirect
from django.urls import resolve


EXCLUDED_APP_NAMES = ("admin", "dev_tools")


class DevToolsLoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response

assert settings.DEV_TOOLS_ENABLED

def __call__(self, request):
assert hasattr(request, "user")

if (
not request.user.is_authenticated
and resolve(request.path).app_name not in EXCLUDED_APP_NAMES
):
return redirect(self.get_login_url())

response = self.get_response(request)

return response

def get_login_url(self):
return settings.DEV_TOOLS_LOGIN_URL or settings.LOGIN_URL
19 changes: 19 additions & 0 deletions src/dev_tools/templates/dev_tools/dialog.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<dialog id="dev-tools-dialog">
<form action="{% url 'dev_tools:change-user' %}?next={{ request.get_full_path }}"
method="post"
novalidate>
{% csrf_token %}
{{ change_user_form }}
<button type="submit">Change user</button>
</form>

<form method="dialog">
<button>Close</button>
</form>
</dialog>

<script>
function openDevToolsDialog() {
document.querySelector("#dev-tools-dialog").showModal();
}
</script>
Empty file.
24 changes: 24 additions & 0 deletions src/dev_tools/templatetags/dev_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django import template
from django.conf import settings
from django.template.loader import render_to_string

from dev_tools.forms import ChangeUserForm


register = template.Library()


@register.simple_tag(takes_context=True)
def dev_tools_dialog(context):
if not hasattr(settings, "DEV_TOOLS_ENABLED") or not (
settings.DEBUG and settings.DEV_TOOLS_ENABLED
):
return ""

request = context["request"]

context = {
"change_user_form": ChangeUserForm(initial={"user": request.user.pk}),
}

return render_to_string("dev_tools/dialog.html", context=context, request=request)
11 changes: 11 additions & 0 deletions src/dev_tools/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path

from dev_tools.views import change_user_view, login_view


app_name = "dev_tools"

urlpatterns = [
path("login", login_view, name="login"),
path("change-user", change_user_view, name="change-user"),
]
66 changes: 66 additions & 0 deletions src/dev_tools/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from functools import wraps

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model, login, logout
from django.core.exceptions import SuspiciousOperation, ValidationError
from django.shortcuts import redirect
from django.views.decorators.http import require_http_methods

from dev_tools.forms import ChangeUserForm


User = get_user_model()


def check_dev_tools_enabled(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not settings.DEV_TOOLS_ENABLED:
raise SuspiciousOperation("Dev tools are not enabled")

return func(*args, **kwargs)

return wrapper


@require_http_methods(["GET"])
@check_dev_tools_enabled
def login_view(request):
assert settings.DEV_TOOLS_DEFAULT_USER

user = User.objects.get(pk=settings.DEV_TOOLS_DEFAULT_USER)
login(request, user)
messages.success(request, f"Automatically logged in as {user}")

return redirect(settings.LOGIN_REDIRECT_URL)


@require_http_methods(["POST"])
@check_dev_tools_enabled
def change_user_view(request):
next_url = request.GET.get("next", settings.LOGIN_REDIRECT_URL)

form = ChangeUserForm(data=request.POST)

if not form.is_valid():
raise ValidationError("Invalid change user form")

if form.cleaned_data["user"]:
new_user = User.objects.get(pk=form.cleaned_data["user"])

login(request, new_user)
messages.success(request, f"Logged in as {new_user}")
else:
logout(request)
messages.success(request, "Logged out")

if is_valid_redirect_url(next_url):
return redirect(next_url)
redirect(settings.LOGIN_REDIRECT_URL)


def is_valid_redirect_url(url: str) -> bool:
if url[0] != "/" and "trade.gov.uk" not in url:
return False
return True

0 comments on commit cf6d341

Please sign in to comment.