Skip to content

Commit

Permalink
4574 add session check redirection (#4636)
Browse files Browse the repository at this point in the history
* Added check in preprocessors for session status
implimented modal in header.html and changed
mixin to use session check.

* Updated to handle request structure

* Updated format with isort and black

* Removed unused import

* Updated url reference for django

* refactored to simplify control logic and updated
tests to handle new test case

* Removed unused import

* Refactored modals and js into seperate files

* Added custom exceptions, middlware handlers

* Updated static pathing for js file

* Updated tests to handle new exception and
realigned naming

* Fixed formatting

* Removed unused import

* Fixed formatting

* Removed unused helper function
  • Loading branch information
anagradova authored Jan 24, 2025
1 parent e84353c commit f0bd3ff
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 30 deletions.
10 changes: 10 additions & 0 deletions backend/audit/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class SessionExpiredException(Exception):
def __init__(self, message="Your session has expired. Please log in again."):
self.message = message
super().__init__(self.message)


class SessionWarningException(Exception):
def __init__(self, message="Your session is about to expire."):
self.message = message
super().__init__(self.message)
13 changes: 7 additions & 6 deletions backend/audit/mixins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Any

from audit.exceptions import SessionExpiredException
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.core.exceptions import PermissionDenied
from django.conf import settings

from .models import Access, SingleAuditChecklist

Expand All @@ -22,12 +24,10 @@ def __init__(self, message, eligible_users):


def check_authenticated(request):
if not hasattr(request, "user"):
raise PermissionDenied(PERMISSION_DENIED_MESSAGE)
if not request.user:
if not hasattr(request, "user") or not request.user:
raise PermissionDenied(PERMISSION_DENIED_MESSAGE)
if not request.user.is_authenticated:
raise PermissionDenied(PERMISSION_DENIED_MESSAGE)
raise SessionExpiredException()


def has_access(sac, user):
Expand Down Expand Up @@ -100,6 +100,7 @@ class CertifyingAuditorRequiredMixin(LoginRequiredMixin):
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
role = "certifying_auditor_contact"
try:

check_authenticated(request)

sac = SingleAuditChecklist.objects.get(report_id=kwargs["report_id"])
Expand Down
6 changes: 4 additions & 2 deletions backend/audit/test_manage_submission_access_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""
Expand Down Expand Up @@ -267,7 +268,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""
Expand Down
3 changes: 2 additions & 1 deletion backend/audit/test_manage_submission_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""
Expand Down
7 changes: 4 additions & 3 deletions backend/audit/test_mixins.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from audit.exceptions import SessionExpiredException
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
Expand Down Expand Up @@ -56,7 +57,7 @@ def test_anonymous_raises(self):

view = self.ViewStub()
self.assertRaises(
PermissionDenied, view.dispatch, request, report_id="not-logged-in"
SessionExpiredException, view.dispatch, request, report_id="not-logged-in"
)

def test_no_access_raises(self):
Expand Down Expand Up @@ -122,7 +123,7 @@ def test_anonymous_raises(self):

view = self.ViewStub()
self.assertRaises(
PermissionDenied, view.dispatch, request, report_id="not-logged-in"
SessionExpiredException, view.dispatch, request, report_id="not-logged-in"
)

def test_no_access_raises(self):
Expand Down Expand Up @@ -217,7 +218,7 @@ def test_anonymous_raises(self):

view = self.ViewStub()
self.assertRaises(
PermissionDenied, view.dispatch, request, report_id="not-logged-in"
SessionExpiredException, view.dispatch, request, report_id="not-logged-in"
)

def test_no_access_raises(self):
Expand Down
3 changes: 2 additions & 1 deletion backend/audit/test_submission_progress_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_phrase_in_page(self):
"""Check for 'General Information form'."""
Expand Down
12 changes: 8 additions & 4 deletions backend/audit/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,8 @@ def test_get_access_denied_for_unauthorized_user(self):
"""Test that GET returns 403 if user is unauthorized"""
self.client.logout()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

@patch("audit.models.SingleAuditChecklist.validate_full")
@patch("audit.views.views.sac_transition")
Expand Down Expand Up @@ -888,7 +889,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""When a request is made for a malformed or nonexistent report_id, a 403 error should be returned"""
Expand Down Expand Up @@ -1525,7 +1527,8 @@ def test_get_login_required(self):
kwargs={"report_id": "12345", "form_section": form_section},
)
)
self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_get_bad_report_id_returns_403(self):
"""Test that uploading with a malformed or nonexistant report_id reutrns 403"""
Expand Down Expand Up @@ -1556,7 +1559,8 @@ def test_login_required(self):
)
)

self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, "home.html")
self.assertTrue(response.context["session_expired"])

def test_bad_report_id_returns_403(self):
"""When a request is made for a malformed or nonexistent report_id, a 403 error should be returned"""
Expand Down
3 changes: 2 additions & 1 deletion backend/config/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from config import settings
from datetime import datetime, timezone

from config import settings


def static_site_url(request):
"""
Expand Down
23 changes: 21 additions & 2 deletions backend/config/middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import boto3
from audit.exceptions import SessionExpiredException, SessionWarningException
from dissemination.file_downloads import file_exists
from django.conf import settings
from django.shortcuts import redirect
import boto3
from django.shortcuts import redirect, render

LOCAL_FILENAME = "./runtime/MAINTENANCE_MODE"
S3_FILENAME = "runtime/MAINTENANCE_MODE"
Expand Down Expand Up @@ -77,3 +78,21 @@ def __call__(self, request):

response = self.get_response(request)
return response


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

def __call__(self, request):
response = self.get_response(request)
return response

def process_exception(self, request, exception):
if isinstance(exception, SessionExpiredException):
context = {"session_expired": True}
return render(request, "home.html", context)
elif isinstance(exception, SessionWarningException):
context = {"show_session_warning_banner": True}
return render(request, "home.html", context)
return None
19 changes: 11 additions & 8 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@
https://docs.djangoproject.com/en/4.0/ref/settings/
"""

from base64 import b64decode
from datetime import datetime, timezone
import json
import logging
import os
import sys
import logging
import json
from .db_url import get_db_url_from_vcap_services
import environs
from cfenv import AppEnv
from audit.get_agency_names import get_agency_names, get_audit_info_lists
from base64 import b64decode
from datetime import datetime, timezone

import dj_database_url
import environs
import newrelic.agent
from audit.get_agency_names import get_agency_names, get_audit_info_lists
from cfenv import AppEnv

from .db_url import get_db_url_from_vcap_services

newrelic.agent.initialize()

Expand Down Expand Up @@ -141,6 +143,7 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"config.middleware.HandleSessionException",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"config.middleware.MaintenanceCheck",
Expand Down
15 changes: 15 additions & 0 deletions backend/static/js/session-expired-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
document.addEventListener("DOMContentLoaded", function () {

const modalTrigger = document.createElement("a");
modalTrigger.setAttribute("href", `#session-expired-modal`);
modalTrigger.setAttribute("data-open-modal", "");
modalTrigger.setAttribute("aria-controls", 'session-expired-modal');
modalTrigger.setAttribute("role", "button");
modalTrigger.className = "sign-in display-flex flex-row";
document.body.appendChild(modalTrigger);

setTimeout(() => {
modalTrigger.click();
modalTrigger.remove();
}, 100);
});
14 changes: 14 additions & 0 deletions backend/static/js/session-warning-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
document.addEventListener("DOMContentLoaded", function () {
const modalTrigger = document.createElement("a");
modalTrigger.setAttribute("href", `#session-warning-modal`);
modalTrigger.setAttribute("data-open-modal", "");
modalTrigger.setAttribute("aria-controls", 'session-warning-modal');
modalTrigger.setAttribute("role", "button");
modalTrigger.className = "sign-in display-flex flex-row";
document.body.appendChild(modalTrigger);

setTimeout(() => {
modalTrigger.click();
modalTrigger.remove();
}, 100);
});
2 changes: 1 addition & 1 deletion backend/templates/includes/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@
</div>
</div>
</section>
{% include "includes/nav_primary.html" %}
{% include "includes/nav_primary.html" %}
7 changes: 6 additions & 1 deletion backend/templates/includes/nav_primary.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="login-modal-heading">You must log in to continue</h2>
<div id="login-modal-description">
<p>
<p class="text-base">
Submitting information to the Federal Audit Clearinghouse requires authentication
which will now be handled by <a href="http://login.gov">Login.gov</a>.
<strong>You cannot use your old Census FAC credentials to access the new GSA
Expand Down Expand Up @@ -235,5 +235,10 @@ <h2 class="usa-modal__heading" id="login-modal-heading">You must log in to conti
{% endif %}
</div>
</div>
{% if session_expired %}
{% include "includes/session_expired_modal.html" %}
{% elif show_session_warning_banner %}
{% include "includes/session_warning_modal.html" %}
{% endif %}
</nav>
</header>
34 changes: 34 additions & 0 deletions backend/templates/includes/session_expired_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% load static %}
<script src="{% static 'compiled/js/session-expired-modal.js' %}"></script>
<div class="usa-modal"
id="session-expired-modal"
aria-labelledby="session-expired-modal-heading"
aria-describedby="session-expired-modal-description">
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="session-expire-modal-heading">Your session has expired</h2>
<div id="session-expired-modal-description">
<p class="text-base">To protect your account, you have been automatically signed out.</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<a href="{% url 'login' %}?next={{ request.path }}">
<button type="button"
class="usa-button sign-in-button"
id="sign-in-expired"
data-close-modal>Authenticate with Login.gov</button>
</a>
</li>
<li class="usa-button-group__item">
<a href="{% url 'Home' %}">
<button type="button"
class="usa-button usa-button--unstyled padding-105 text-center"
data-close-modal>Cancel</button>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
51 changes: 51 additions & 0 deletions backend/templates/includes/session_warning_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% load static %}
<script src="{% static 'compiled/js/session-warning-modal.js' %}"></script>
<div class="usa-modal usa-modal"
id="session-warning-modal"
aria-labelledby="session-warning-heading"
aria-describedby="session-warning-description">
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="session-warning-heading">You will be signed out soon</h2>
<div id="session-warning-description">
<p class="text-base">Your session is about to expire due to inactivity.</p>
<li class="usa-icon-list__item">
<div class="usa-icon-list__icon text-warning-dark">
<svg class="usa-icon" aria-hidden="true" role="img">
{% uswds_sprite "error" %}
</svg>
</div>
<div class="usa-icon-list__content">
Time Remaining: {{ formatted_time }}
</div>
</li>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<a href="#">
<button type="button"
class="usa-button sign-in-button"
id="continue-session"
data-close-modal>Continue Session</button>
</a>
</li>
<li class="usa-button-group__item">
<a href="{% url 'logout' %}">
<button type="button"
class="usa-button usa-button--unstyled padding-105 text-center"
data-close-modal>Sign out now</button>
</a>
</li>
</ul>
</div>
</div>
<button class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
{% uswds_sprite "close" %}
</svg>
</button>
</div>
</div>

0 comments on commit f0bd3ff

Please sign in to comment.