Add /whoami view for logged-in user

This commit is contained in:
Seamus Johnston 2022-09-13 09:15:40 -05:00 committed by Neil Martinsen-Burrell
parent cbf39aa3c3
commit f147f8c2ab
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
16 changed files with 375 additions and 167 deletions

81
src/djangooidc/status.py Normal file
View file

@ -0,0 +1,81 @@
"""
Descriptive HTTP status codes, for code readability.
See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
And RFC 6585 - http://tools.ietf.org/html/rfc6585
And RFC 4918 - https://tools.ietf.org/html/rfc4918
"""
from __future__ import unicode_literals
def is_informational(code):
return code >= 100 and code <= 199
def is_success(code):
return code >= 200 and code <= 299
def is_redirect(code):
return code >= 300 and code <= 399
def is_client_error(code):
return code >= 400 and code <= 499
def is_server_error(code):
return code >= 500 and code <= 599
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_507_INSUFFICIENT_STORAGE = 507
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
<style>
table, th, td {
border: 1px solid black;
}
table {
border-collapse: collapse;
}
.ooidc-block {
border: solid 1px black;
margin: 5px;
padding: 3px;
}
.ooidc-block-title {
display: block;
font-style: italic;
}
</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,33 @@
{% extends "djangooidc/base.html" %}
{% block title %}Error{% endblock %}
{% block content %}
<div>
<div>OpenID Connect authentication has failed</div>
<div>
<div><span>Error message is: </span>{{ error }}</div>
{% if callback %}
<div>Query content was:</div>
<div>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key,value in callback.items %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends "djangooidc/base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div>
<div>Please log in with one of the following methods:</div>
<div class="ooidc-block">
<div class="ooidc-block-title">Application login</div>
<form method="post">
{% csrf_token %}
{{ ilform }}
<input type="hidden" name="next" value="{{ next }}" />
<input name="internal_login" type="submit">
</form>
</div>
{% if op_list %}
<div class="ooidc-block">
<div class="ooidc-block-title">Log in with one of the following systems</div>
{% for op_name in op_list %}
<a href="{% url 'openid_with_op_name' op_name=op_name %}">{{ op_name }}</a>&nbsp;
{% endfor %}
</div>
{% endif %}
{% if dynamic %}
<div class="ooidc-block">
<div class="ooidc-block-title">Log in in with an OpenID Connect provider of your choice</div>
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit">
</form>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -10,10 +10,6 @@ services:
- db
working_dir: /app
entrypoint: python /app/docker_entrypoint.py
deploy:
restart_policy:
condition: on-failure
max_attempts: 5
environment:
# Send stdout and stderr straight to the terminal without buffering
- PYTHONUNBUFFERED=yup
@ -36,8 +32,7 @@ services:
- "8080:8080"
# command: "python"
command: >
bash -c " python manage.py migrate &&
python manage.py runserver 0.0.0.0:8080"
bash -c " python manage.py migrate && python manage.py runserver 0.0.0.0:8080"
db:
image: postgres:latest

View file

@ -26,4 +26,3 @@ i.e.
p {
color: color('blue-10v');
}

View file

@ -164,6 +164,7 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"registrar.context_processors.language_code",
"registrar.context_processors.canonical_path",
],
},
},

View file

@ -7,7 +7,7 @@ For more information see:
from django.contrib import admin
from django.urls import include, path
from registrar.views import health, index
from registrar.views import health, index, whoami
urlpatterns = [
path("", index.index, name="home"),

View file

@ -11,3 +11,12 @@ def language_code(request):
TEMPLATES dict of our settings file).
"""
return {"LANGUAGE_CODE": settings.LANGUAGE_CODE}
def canonical_path(request):
"""Add a canonical URL to the template context.
To make a correct "rel=canonical" link in the HTML page, we need to
construct an absolute URL for the page, and we can't do that in the
template itself, so we do it here and pass the information on.
"""
return {"CANONICAL_PATH": request.build_absolute_uri(request.path)}

View file

@ -7,28 +7,26 @@
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>
{% block title %}{% endblock %}
{{ site.name }}
.gov Registrar
{% block extra_title %}{% endblock %}
</title>
<meta name="description" content="{% block description %}{% endblock %}">
{% block viewport_meta %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block extra_meta %}{% endblock extra_meta %}
{# TO-DO: Determine if <link rel="manifest" href="site.webmanifest"> is desirable #}
{# TO-DO: set defaults for these #}
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
<link rel="apple-touch-icon" href="{% static 'img/touch-icon.png' %}">
{% block css %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
{% endblock %}
{% block canonical %}
<link rel="canonical" href="{{ current_path }}">
<link rel="canonical" href="{{ CANONICAL_PATH }}">
{% endblock %}
@ -49,154 +47,136 @@
<script src="{% static 'js/uswds.min.js' %}" defer></script>
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
<section class="usa-banner" aria-label="Official government website">
<div class="usa-accordion">
<header class="usa-banner__header">
<div class="usa-banner__inner">
<div class="grid-col-auto">
<img
class="usa-banner__header-flag"
src="{% static 'img/us_flag_small.png' %}"
alt="U.S. flag"
/>
</div>
<div class="grid-col-fill tablet:grid-col-auto">
<p class="usa-banner__header-text">
An official website of the United States government
</p>
<p class="usa-banner__header-action" aria-hidden="true">
Heres how you know
</p>
</div>
<button
class="usa-accordion__button usa-banner__button"
aria-expanded="false"
aria-controls="gov-banner-default-default"
>
<span class="usa-banner__button-text">Heres how you know</span>
</button>
</div>
</header>
<div
class="usa-banner__content usa-accordion__content"
id="gov-banner-default-default"
>
<div class="grid-row grid-gap-lg">
<div class="usa-banner__guidance tablet:grid-col-6">
<img
class="usa-banner__icon usa-media-block__img"
src="{% static 'img/icon-dot-gov.svg' %}"
role="img"
alt=""
aria-hidden="true"
/>
<div class="usa-media-block__body">
<p>
<strong>Official websites use .gov</strong><br />A
<strong>.gov</strong> website belongs to an official government
organization in the United States.
<section class="usa-banner" aria-label="Official government website">
<div class="usa-accordion">
<header class="usa-banner__header">
<div class="usa-banner__inner">
<div class="grid-col-auto">
<img class="usa-banner__header-flag" src="{% static 'img/us_flag_small.png' %}" alt="U.S. flag" />
</div>
<div class="grid-col-fill tablet:grid-col-auto">
<p class="usa-banner__header-text">
An official website of the United States government
</p>
<p class="usa-banner__header-action" aria-hidden="true">
Heres how you know
</p>
</div>
<button class="usa-accordion__button usa-banner__button" aria-expanded="false"
aria-controls="gov-banner-default-default">
<span class="usa-banner__button-text">Heres how you know</span>
</button>
</div>
<div class="usa-banner__guidance tablet:grid-col-6">
<img
class="usa-banner__icon usa-media-block__img"
src="{% static 'img/icon-https.svg' %}"
role="img"
alt=""
aria-hidden="true"
/>
<div class="usa-media-block__body">
<p>
<strong>Secure .gov websites use HTTPS</strong><br />A
<strong>lock</strong> (
<span class="icon-lock"
><svg
xmlns="http://www.w3.org/2000/svg"
width="52"
height="64"
viewBox="0 0 52 64"
class="usa-banner__lock-image"
role="img"
aria-labelledby="banner-lock-title-default banner-lock-description-default"
focusable="false"
>
<title id="banner-lock-title-default">Lock</title>
<desc id="banner-lock-description-default">A locked padlock</desc>
<path
fill="#000000"
fill-rule="evenodd"
d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"
/>
</svg> </span
>) or <strong>https://</strong> means youve safely connected to
the .gov website. Share sensitive information only on official,
secure websites.
</p>
</header>
<div class="usa-banner__content usa-accordion__content" id="gov-banner-default-default">
<div class="grid-row grid-gap-lg">
<div class="usa-banner__guidance tablet:grid-col-6">
<img class="usa-banner__icon usa-media-block__img" src="{% static 'img/icon-dot-gov.svg' %}" role="img"
alt="" aria-hidden="true" />
<div class="usa-media-block__body">
<p>
<strong>Official websites use .gov</strong><br />A
<strong>.gov</strong> website belongs to an official government
organization in the United States.
</p>
</div>
</div>
<div class="usa-banner__guidance tablet:grid-col-6">
<img class="usa-banner__icon usa-media-block__img" src="{% static 'img/icon-https.svg' %}" role="img" alt=""
aria-hidden="true" />
<div class="usa-media-block__body">
<p>
<strong>Secure .gov websites use HTTPS</strong><br />A
<strong>lock</strong> (
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64"
viewBox="0 0 52 64" class="usa-banner__lock-image" role="img"
aria-labelledby="banner-lock-title-default banner-lock-description-default" focusable="false">
<title id="banner-lock-title-default">Lock</title>
<desc id="banner-lock-description-default">A locked padlock</desc>
<path fill="#000000" fill-rule="evenodd"
d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z" />
</svg> </span>) or <strong>https://</strong> means youve safely connected to
the .gov website. Share sensitive information only on official,
secure websites.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
{% block banner %}
<header class="usa-header usa-header-extended" role="banner">
<header class="usa-header usa-header-basic" role="navigation">
<div class="usa-nav-container">
<div class="usa-navbar">
{% block logo %}
<div class="usa-logo" id="extended-logo">
<em class="usa-logo-text">
<a href="/"
title="Home"
aria-label="Home">
{% block site_name %}{{ site.name }}{% endblock %}
</a>
</em>
</div>
<div class="usa-logo" id="extended-logo">
<em class="usa-logo__text">
<a href="/" title="Home" aria-label="Home">
{% block site_name %}Home{% endblock %}
</a>
</em>
</div>
{% endblock %}
<button class="usa-menu-btn">Menu</button>
</div>
{% block usa_nav %}
{% block usa_nav_secondary %}{% endblock %}
<nav>
<button type="button" class="usa-nav__close">
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
{% if user.is_authenticated %}
User: <a href="/whoami">{{ user.get_username }}</a>
{% else %}
<a href="/openid/openid/login">Sign in</a>
{% endif %}
</li>
</ul>
</nav>
{% block usa_nav_secondary %}{% endblock %}
{% endblock %}
</header>
</div>
</header>
{% endblock banner %}
{% block usa_overlay %}<div class="usa-overlay"></div>{% endblock %}
<div id="wrapper">
{% block messages %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
{% block messages %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
{% block section_nav %}{% endblock %}
{% block section_nav %}{% endblock %}
<main id="main-content">
{% block hero %}{% endblock %}
{% block content %}{% endblock %}
</main>
<main id="main-content">
{% block hero %}{% endblock %}
{% block content %}{% endblock %}
</main>
<div role="complementary">{% block complementary %}{% endblock %}</div>
<div role="complementary">{% block complementary %}{% endblock %}</div>
{% block content_bottom %}{% endblock %}
{% block content_bottom %}{% endblock %}
</div>
<footer id="footer" role="contentinfo">
{% block footer_nav %}
{% endblock %}
{% block footer %}
<div>
<p class="copyright">&copy; {% now "Y" %} CISA .gov Registrar</p>
</div>
<footer id="footer" role="contentinfo">
{% block footer_nav %}
{% endblock %}
{% block footer %}
<div>
<p class="copyright">&copy; {{ now.year }} {{ site.name }}</p>
</div>
{% endblock %}
</footer>
{% endblock %}
</footer>
</div> <!-- /#wrapper -->
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
@ -208,8 +188,6 @@
{% block extrascript %}{% endblock %}
{# asynchronous analytics #}
<script async id="_fed_an_ua_tag" src="https://dap.digitalgov.gov/Universal­Federated­Analytics­M
in.js?agency={{ AGENCY }}"></script>
</body>
</html>

View file

@ -3,14 +3,14 @@
{% block title %} Hello {% endblock %}
{% block hero %}
<section class="usa-hero">
<div class="usa-grid">
<div class="usa-hero-callout usa-section-dark">
<h2>
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
</h2>
<section class="usa-hero">
<div class="usa-grid">
<div class="usa-hero-callout usa-section-dark">
<h2>
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
</h2>
</div>
</section>
</section>
{% endblock %}
{% block content %}
@ -23,4 +23,4 @@
<p><a href="/openid/login/">Click here to log in.</a></p>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -2,17 +2,8 @@
{% extends 'base.html' %}
{% block title %} Hello {% endblock %}
{% block hero %}
<section class="usa-hero">
<div class="usa-grid">
<div class="usa-hero-callout usa-section-dark">
<h2>
<span class="usa-hero-callout-alt">This is sample content.</span>
This is only sample content.
</h2>
<p> {{ name }} You'll want to replace it with content of your own.</p>
<button class="usa-button usa-button--accent-cool">Click a usa button</button>
</div>
</div>
</section>
{% block content %}
<p> Hello {{ user.last_name|default:"No last name given" }}, {{ user.first_name|default:"No first name given" }} &lt;{{ user.email }}&gt;! </p>
<p><a href="/openid/logout">Click here to log out</a></p>
{% endblock %}

View file

@ -1,11 +1,38 @@
from django.test import Client, TestCase
from django.contrib.auth.models import User
class HealthTest(TestCase):
class TestViews(TestCase):
def setUp(self):
self.client = Client()
def test_health_check_endpoint(self):
response = self.client.get("/health/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "OK")
self.assertContains(response, "OK", status_code=200)
def test_home_page(self):
"""Home page should be available without a login."""
response = self.client.get("/")
self.assertContains(response, "registrar", status_code=200)
self.assertContains(response, "log in")
def test_whoami_page(self):
"""User information appears on the whoami page."""
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
user = User.objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(user)
response = self.client.get("/whoami")
self.assertContains(response, first_name)
self.assertContains(response, last_name)
self.assertContains(response, email)
def test_whoami_page_no_user(self):
"""Whoami page not accessible without a logged-in user."""
response = self.client.get("/whoami")
self.assertEqual(response.status_code, 302)
self.assertIn("?next=/whoami", response.headers["Location"])

View file

@ -1,7 +1,26 @@
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
def health(request):
return HttpResponse(
'<html lang="en"><head><title>OK - Get.gov</title></head><body>OK</body>'
)
@login_required
def home(request):
return render(
request,
"testapp/result.html",
{
"userinfo": request.session["userinfo"]
if "userinfo" in request.session.keys()
else None
},
)
def unprotected(request):
return render(request, "testapp/unprotected.html")

View file

@ -1,6 +1,7 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
def index(request):
context = {"name": "World!"}
return render(request, "whoami.html", context)
"""This page is available to anyone without logging in."""
return render(request, "home.html")

View file

@ -0,0 +1,8 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@login_required
def whoami(request):
"""This is the first page someone goes to after logging in."""
return render(request, "whoami.html")