Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DBC22-2535 IDIR login for admin/cms #784

Merged
merged 3 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/backend/apps/authentication/adapters.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter


class AccountAdapter(DefaultAccountAdapter):
''' Governs accounts internally, for things like redirect urls. '''

def get_login_redirect_url(self, request):
return settings.FRONTEND_BASE_URL

def get_logout_redirect_url(self, request):
return settings.FRONTEND_BASE_URL
if request.path.startswith('/drivebc-'):
return '/' + request.path.split('/')[1] + '/'
return settings.FRONTEND_BASE_URL

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "admin/login.html" %}

{% block content %}
<p>You do not have permission to view this page.</p>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "admin/login.html" %}

{% block content %}
<p>An email has been sent to the admins. Please wait for them to contact you.</p>
{% endblock %}
14 changes: 14 additions & 0 deletions src/backend/apps/authentication/templates/admin/idir_login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "admin/login.html" %}

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">DriveBC Administration</a></h1>
{% endblock %}

{% block content %}

<p>This part of the DriveBC website requires you to be authenticated with your
IDIR, using MFA</p>

<p style="text-align: center"><a class="button" href="/accounts/oidc/idir/login/?process=login&next=%2Fdrivebc-admin%2F&auth_params=kc_idp_hint=azureidir">Log in with IDIR</a></p>

{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "admin/login.html" %}

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">DriveBC Administration</a></h1>
{% endblock %}

{% block content %}
<p>To access the DriveBC Admin site, you need to be granted access.</p>

<p>Click the button below to trigger an access request to be sent to the admins
for your IDIR.</p>

<div style="text-align: center">
<form method="post" action="{% url 'admin-access-requested' %}">
{% csrf_token %}
<button class="button" type="submit" style="padding: 4px 5px;">Request Access</button></p>
</form>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>IDIR user {{ name }} ({{ email }}) is requesting access to the DriveBC admin
interface. You can access their user record at the following link to toggle the
staff flag on their account:</p>

<p><a href="{{ url }}">{{ url }}</a></p>

<p>The system will NOT notify them of the change; you need to tell them yourself.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
IDIR user {{ name }} ({{ email }}) is requesting access to the DriveBC admin
interface. You can access their user record at the following link to toggle the
staff flag on their account:

{{ url }}

The system will NOT notify them of the change; you need to tell them yourself.
37 changes: 36 additions & 1 deletion src/backend/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

import environ
from apps.webcam.models import Webcam
from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import EmailMultiAlternatives
from django.db.utils import IntegrityError
from django.http import HttpResponseRedirect
from django.shortcuts import reverse
from django.shortcuts import render, reverse
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
Expand All @@ -19,6 +20,8 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from apps.webcam.models import Webcam

from .models import DriveBCUser, FavouritedCameras, SavedRoutes
from .serializers import FavouritedCamerasSerializer, SavedRoutesSerializer

Expand All @@ -27,6 +30,38 @@
env = environ.Env()


def request_access(request):
return render(request, "admin/request_access.html")


def access_requested(request):

if request.method == 'POST':
app = request.user._meta.app_label
model = request.user._meta.model_name
path = reverse('admin:%s_%s_change' % (app, model), args=[request.user.id])
url = settings.FRONTEND_BASE_URL + path[1:]
first = request.user.first_name
last = request.user.last_name
name = f'{first} {last}'
context = {'name': name, 'email': request.user.email, 'url': url, }

text = render_to_string('email/request_admin_access.txt', context)
html = render_to_string('email/request_admin_access.html', context)

msg = EmailMultiAlternatives(
f'{name} requests access to DriveBC admin',
text,
settings.DRIVEBC_FEEDBACK_EMAIL_DEFAULT,
settings.ACCESS_REQUEST_RECEIVERS,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace this instance too

)
msg.attach_alternative(html, 'text/html')
msg.send()
return HttpResponseRedirect(request.path)

return render(request, "admin/access_requested.html")


class FavouritedCamerasViewset(viewsets.ModelViewSet):
queryset = FavouritedCameras.objects.all()
serializer_class = FavouritedCamerasSerializer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>IDIR user {{ name }} ({{ email }}) is requesting access to the Wagtail admin
interface. You can access their user record at the following link to add them
to the editors group:</p>

<p><a href="{{ url }}">{{ url }}</a></p>

<p>The system will NOT notify them of the change; you need to tell them yourself.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
IDIR user {{ name }} ({{ email }}) is requesting access to the Wagtail admin
interface. You can access their user record at the following link to add them
to the editors group:

{{ url }}

The system will NOT notify them of the change; you need to tell them yourself.
24 changes: 24 additions & 0 deletions src/backend/apps/cms/templates/wagtailadmin/access_denied.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "admin/login.html" %}

{% block branding %}<h1 id="site-name">DriveBC CMS</h1>{% endblock %}

{% block content %}

{% if is_non_idir_login %}
<p>
You do not have permission to view this page because you're logged in with
an account that is not an IDIR MFA account.
</p>
<p>
You can sign out of your current account by clicking the button below.
You'll be redirected to the login page for IDIR.
</p>

<form method="POST" action="/drivebc-cms/logout/">
{% csrf_token %}
<button type="submit" class="button" style="padding: 4px 5px;">Log out</button>
</form>
{% else %}
<p>You do not have permission to view this page.</p>
{% endif %}
{% endblock %}
12 changes: 12 additions & 0 deletions src/backend/apps/cms/templates/wagtailadmin/access_requested.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "wagtailadmin/base.html" %}

{% block titletag %}Access Requested{% endblock %}

{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Access Requested" %}

<div class="nice-padding">
<p>A message has been sent to the CMS admins. Please wait for them to
contact you.</p>
</div>
{% endblock %}
15 changes: 15 additions & 0 deletions src/backend/apps/cms/templates/wagtailadmin/request_access.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% load i18n wagtailadmin_tags %}

{% block request_access %}
<h2 class="w-panel_header">Need to be granted access</h2>

<p>You can see this page because you're logged in with an IDIR, but you do
not have access to any functionality yet because you need to be added to the
<em>editors</em> group.</p>

<p>Click the button below to send a message to CMS admins requesting access for your IDIR.</p>

<form method="POST" action="access-requested">
<div><button class="button" type="submit">Request Access</button></div>
</form>
{% endblock %}
38 changes: 37 additions & 1 deletion src/backend/apps/cms/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from functools import wraps

from allauth.account.decorators import secure_admin_login
from allauth.account.views import LogoutView as AllauthLogoutView
from django.conf import settings
from django.shortcuts import redirect
from django.urls import include, path
from rest_framework import routers
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.admin.auth import reject_request, permission_denied
from wagtail.admin.views.account import LoginView, LogoutView as WagtailLogoutView
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
from wagtail.images.api.v2.views import ImagesAPIViewSet
from wagtail.utils.urlpatterns import decorate_urlpatterns

from .views import AdvisoryAPI, BulletinAPI
from .views import AdvisoryAPI, BulletinAPI, access_denied_idir

wagtail_api_router = WagtailAPIRouter('wagtailapi')
wagtail_api_router.register_endpoint('pages', PagesAPIViewSet)
Expand All @@ -19,7 +28,34 @@
cms_api_router.register('advisories', AdvisoryAPI)
cms_api_router.register('bulletins', BulletinAPI)


def require_idir_auth(view_func):
def decorated_view(request, *args, **kwargs):
user = request.user

if user.is_anonymous:
return reject_request(request)

if user.username.endswith('azureidir'):
return view_func(request, *args, **kwargs)

return redirect("cms_denied_idir")

return decorated_view

if settings.FORCE_IDIR_AUTHENTICATION:
login_view = secure_admin_login(LoginView.as_view())
logout_view = AllauthLogoutView.as_view()
decorate_urlpatterns(wagtailadmin_urls.urlpatterns, require_idir_auth)
else:
login_view = LoginView.as_view()
logout_view = WagtailLogoutView.as_view()


urlpatterns = [
path("login/", login_view, name="cms_login"),
path("logout/", logout_view, name="cms_logout"),
path("denied", access_denied_idir, name="cms_denied_idir"),
path('', include(wagtailadmin_urls)),
path('documents', include(wagtaildocs_urls)),
path('pages', include(wagtail_urls)),
Expand Down
46 changes: 45 additions & 1 deletion src/backend/apps/cms/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.http import HttpResponseRedirect
from django.shortcuts import render, reverse
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
from rest_framework import viewsets

from apps.cms.models import Advisory, Bulletin
from apps.cms.serializers import (
AdvisorySerializer,
Expand All @@ -6,7 +14,6 @@
)
from apps.shared.enums import CacheKey, CacheTimeout
from apps.shared.views import CachedListModelMixin
from rest_framework import viewsets


class CMSViewSet(viewsets.ReadOnlyModelViewSet):
Expand Down Expand Up @@ -36,3 +43,40 @@

class BulletinAPI(BulletinTestAPI):
serializer_class = BulletinSerializer


@csrf_exempt
def access_requested(request):

if request.method == 'POST':
app = request.user._meta.app_label
model = request.user._meta.model_name
path = reverse('admin:%s_%s_change' % (app, model), args=[request.user.id])
url = settings.FRONTEND_BASE_URL + path[1:]
first = request.user.first_name
last = request.user.last_name
name = f'{first} {last}'
context = {'name': name,
'email': request.user.email,
'url': url, }

text = render_to_string('email/request_wagtail_access.txt', context)
html = render_to_string('email/request_wagtail_access.html', context)

msg = EmailMultiAlternatives(
f'{name} requests access to Wagtail admin',
text,
settings.DRIVEBC_FEEDBACK_EMAIL_DEFAULT,
settings.ACCESS_REQUEST_RECEIVERS,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace instances of these FROM emails to env("DRIVEBC_FEEDBACK_EMAIL_DEFAULT")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes.

)
msg.attach_alternative(html, 'text/html')
msg.send()
return HttpResponseRedirect(request.path)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
return render(request, 'wagtailadmin/access_requested.html')


def access_denied_idir(request):
return render(request, 'wagtailadmin/access_denied.html', context={
"is_non_idir_login": True,
})
Loading
Loading