Skip to content

Commit 34e2df9

Browse files
authored
Merge pull request #135 from LandRegistry/nginx
Use NGINX reverse proxy server
2 parents 1fa4820 + 8c2f0b0 commit 34e2df9

24 files changed

+325
-240
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
pip install -r requirements_dev.txt
2828
pip install -r requirements.txt
2929
- name: Check dependencies for known security vulnerabilities
30-
run: safety check -r requirements.txt
30+
run: pip-audit -r requirements.txt
3131
- name: Check code for potential security vulnerabilities
3232
run: bandit -r . -x /tests
3333
- name: Check code formatting

Dockerfile

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
FROM python:3.12-slim
22

3-
RUN useradd containeruser
3+
RUN useradd appuser
44

5-
WORKDIR /home/containeruser
6-
7-
COPY app app
8-
COPY govuk-frontend-flask.py config.py docker-entrypoint.sh requirements.txt ./
9-
RUN pip install -r requirements.txt \
10-
&& chmod +x docker-entrypoint.sh \
11-
&& chown -R containeruser:containeruser ./
5+
WORKDIR /home/appuser
126

137
# Set environment variables
148
ENV FLASK_APP=govuk-frontend-flask.py \
159
PYTHONDONTWRITEBYTECODE=1 \
1610
PYTHONUNBUFFERED=1
1711

18-
USER containeruser
12+
COPY app app
13+
COPY govuk-frontend-flask.py config.py requirements.txt ./
14+
RUN pip install -r requirements.txt \
15+
&& chown -R appuser:appuser ./
1916

20-
EXPOSE 9876
21-
ENTRYPOINT ["./docker-entrypoint.sh"]
17+
USER appuser

README.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# GOV.UK Frontend Flask
22

3-
![govuk-frontend 5.4.0](https://img.shields.io/badge/govuk--frontend%20version-5.4.0-005EA5?logo=gov.uk&style=flat)
3+
![govuk-frontend 5.6.0](https://img.shields.io/badge/govuk--frontend%20version-5.6.0-005EA5?logo=gov.uk&style=flat)
44

55
**GOV.UK Frontend Flask is a [community tool](https://design-system.service.gov.uk/community/resources-and-tools/) of the [GOV.UK Design System](https://design-system.service.gov.uk/). The Design System team is not responsible for it and cannot support you with using it. Contact the [maintainers](#contributors) directly if you need [help](#support) or you want to request a feature.**
66

@@ -53,7 +53,7 @@ python -c 'import secrets; print(secrets.token_hex())'
5353
docker compose up --build
5454
```
5555

56-
You should now have the app running on <https://localhost:9876/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.
56+
You should now have the app running on <https://localhost/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.
5757

5858
## Demos
5959

@@ -67,19 +67,52 @@ To run the tests:
6767
python -m pytest --cov=app --cov-report=term-missing --cov-branch
6868
```
6969

70+
## Environment
71+
72+
```mermaid
73+
flowchart TB
74+
cache1(Redis):::CACHE
75+
Client
76+
prox1(NGINX):::PROXY
77+
web1(Flask app):::WEB
78+
web2[/Static files/]:::WEB
79+
80+
Client <-- https:443 --> prox1 <-- http:5000 --> web1
81+
prox1 -- Read only --> web2
82+
web1 -- Write --> web2
83+
web1 <-- redis:6379 --> cache1
84+
85+
subgraph Proxy container
86+
prox1
87+
end
88+
89+
subgraph Web container
90+
web1
91+
web2
92+
end
93+
94+
subgraph Cache container
95+
cache1
96+
end
97+
98+
classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px
99+
classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px
100+
classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px
101+
```
102+
70103
## Features
71104

72105
Please refer to the specific packages documentation for more details.
73106

74107
### Asset management
75108

76-
Custom CSS and JavaScript files are merged and compressed using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single compressed file to both `app/static/dist/css` and `app/static/dist/js` respectively.
109+
Custom CSS and JavaScript files are merged and minified using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single minified file to both `app/static/dist/css` and `app/static/dist/js` respectively.
77110

78111
CSS is [minified](https://en.wikipedia.org/wiki/Minification_(programming)) using [CSSMin](https://github.com/zacharyvoase/cssmin) and JavaScript is minified using [JSMin](https://github.com/tikitu/jsmin/). This removes all whitespace characters, comments and line breaks to reduce the size of the source code, making its transmission over a network more efficient.
79112

80113
### Cache busting
81114

82-
Merged and compressed assets are browser cache busted on update by modifying their URL with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour.
115+
Merged and minified assets are browser cache busted on update by modifying the filename with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour.
83116

84117
### Forms generation and validation
85118

@@ -101,20 +134,21 @@ CSRF errors are handled by creating a [flash message](#flash-messages) notificat
101134

102135
### HTTP security headers
103136

104-
Uses [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to set HTTP headers that can help protect against a few common web application security issues.
105-
106-
- Forces all connections to `https`, unless running with debug enabled.
137+
- Forces all connections to `https`.
107138
- Enables [HTTP Strict Transport Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security).
108139
- Sets Flask's session cookie to `secure`, so it will never be set if your application is somehow accessed via a non-secure connection.
109140
- Sets Flask's session cookie to `httponly`, preventing JavaScript from being able to access its content.
110141
- Sets [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) to `SAMEORIGIN` to avoid [clickjacking](https://en.wikipedia.org/wiki/Clickjacking).
111-
- Sets [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) to enable a cross site scripting filter for IE and Safari (note Chrome has removed this and Firefox never supported it).
112142
- Sets [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) to prevent content type sniffing.
113143
- Sets a strict [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) of `strict-origin-when-cross-origin` that governs which referrer information should be included with requests made.
114144

115145
### Content Security Policy
116146

117-
A strict default [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set using [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application.
147+
A strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application by default.
148+
149+
### Permissions Policy
150+
151+
A strict [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) is set to deny the use of browser features by default.
118152

119153
### Response compression
120154

app/__init__.py

Lines changed: 16 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
from flask import Flask
22
from flask_assets import Bundle, Environment
3-
from flask_compress import Compress
43
from flask_limiter import Limiter
54
from flask_limiter.util import get_remote_address
6-
from flask_talisman import Talisman
75
from flask_wtf.csrf import CSRFProtect
86
from govuk_frontend_wtf.main import WTFormsHelpers
97
from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader
8+
from werkzeug.middleware.proxy_fix import ProxyFix
109

1110
from config import Config
1211

1312
assets = Environment()
14-
compress = Compress()
1513
csrf = CSRFProtect()
16-
limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"])
17-
talisman = Talisman()
14+
limiter = Limiter(
15+
get_remote_address,
16+
default_limits=["2 per second", "60 per minute"],
17+
)
1818

1919

2020
def create_app(config_class=Config):
@@ -33,74 +33,25 @@ def create_app(config_class=Config):
3333
),
3434
]
3535
)
36-
37-
# Set content security policy
38-
csp = {
39-
"default-src": "'self'",
40-
"script-src": [
41-
"'self'",
42-
"'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw='",
43-
"'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='",
44-
],
45-
}
46-
47-
# Set permissions policy
48-
permissions_policy = {
49-
"accelerometer": "()",
50-
"ambient-light-sensor": "()",
51-
"autoplay": "()",
52-
"battery": "()",
53-
"camera": "()",
54-
"cross-origin-isolated": "()",
55-
"display-capture": "()",
56-
"document-domain": "()",
57-
"encrypted-media": "()",
58-
"execution-while-not-rendered": "()",
59-
"execution-while-out-of-viewport": "()",
60-
"fullscreen": "()",
61-
"geolocation": "()",
62-
"gyroscope": "()",
63-
"keyboard-map": "()",
64-
"magnetometer": "()",
65-
"microphone": "()",
66-
"midi": "()",
67-
"navigation-override": "()",
68-
"payment": "()",
69-
"picture-in-picture": "()",
70-
"publickey-credentials-get": "()",
71-
"screen-wake-lock": "()",
72-
"sync-xhr": "()",
73-
"usb": "()",
74-
"web-share": "()",
75-
"xr-spatial-tracking": "()",
76-
"clipboard-read": "()",
77-
"clipboard-write": "()",
78-
"gamepad": "()",
79-
"speaker-selection": "()",
80-
"conversion-measurement": "()",
81-
"focus-without-user-activation": "()",
82-
"hid": "()",
83-
"idle-detection": "()",
84-
"interest-cohort": "()",
85-
"serial": "()",
86-
"sync-script": "()",
87-
"trust-token-redemption": "()",
88-
"unload": "()",
89-
"window-management": "()",
90-
"vertical-scroll": "()",
91-
}
36+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
9237

9338
# Initialise app extensions
9439
assets.init_app(app)
95-
compress.init_app(app)
9640
csrf.init_app(app)
9741
limiter.init_app(app)
98-
talisman.init_app(app, content_security_policy=csp, permissions_policy=permissions_policy)
9942
WTFormsHelpers(app)
10043

10144
# Create static asset bundles
102-
css = Bundle("src/css/*.css", filters="cssmin", output="dist/css/custom-%(version)s.min.css")
103-
js = Bundle("src/js/*.js", filters="jsmin", output="dist/js/custom-%(version)s.min.js")
45+
css = Bundle(
46+
"src/css/*.css",
47+
filters="cssmin",
48+
output="dist/css/custom-%(version)s.min.css",
49+
)
50+
js = Bundle(
51+
"src/js/*.js",
52+
filters="jsmin",
53+
output="dist/js/custom-%(version)s.min.js",
54+
)
10455
if "css" not in assets:
10556
assets.register("css", css)
10657
if "js" not in assets:

app/demos/custom_validators.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33

44
class RequiredIf(InputRequired):
5-
def __init__(self, other_field_name, other_field_value, *args, **kwargs):
5+
def __init__(
6+
self,
7+
other_field_name,
8+
other_field_value,
9+
*args,
10+
**kwargs,
11+
):
612
self.other_field_name = other_field_name
713
self.other_field_value = other_field_value
814

app/demos/forms.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ class BankDetailsForm(FlaskForm):
4545
widget=GovTextInput(),
4646
validators=[
4747
InputRequired(message="Enter a sort code"),
48-
Regexp(regex=r"\d{6}", message="Enter a valid sort code like 309430"),
48+
Regexp(
49+
regex=r"\d{6}",
50+
message="Enter a valid sort code like 309430",
51+
),
4952
],
5053
description="Must be 6 digits long",
5154
)
@@ -54,8 +57,15 @@ class BankDetailsForm(FlaskForm):
5457
widget=GovTextInput(),
5558
validators=[
5659
InputRequired(message="Enter an account number"),
57-
Regexp(regex=r"\d{6,8}", message="Enter a valid account number like 00733445"),
58-
Length(min=6, max=8, message="Account number must be between 6 and 8 digits"),
60+
Regexp(
61+
regex=r"\d{6,8}",
62+
message="Enter a valid account number like 00733445",
63+
),
64+
Length(
65+
min=6,
66+
max=8,
67+
message="Account number must be between 6 and 8 digits",
68+
),
5969
],
6070
description="Must be between 6 and 8 digits long",
6171
)
@@ -118,7 +128,10 @@ class CreateAccountForm(FlaskForm):
118128
widget=GovTextInput(),
119129
validators=[
120130
InputRequired(message="Enter an email address"),
121-
Length(max=256, message="Email address must be 256 characters or fewer"),
131+
Length(
132+
max=256,
133+
message="Email address must be 256 characters or fewer",
134+
),
122135
Email(message="Enter an email address in the correct format, like name@example.com"),
123136
],
124137
description="You'll need this email address to sign in to your account",
@@ -139,7 +152,10 @@ class CreateAccountForm(FlaskForm):
139152
widget=GovPasswordInput(),
140153
validators=[
141154
InputRequired(message="Enter a password"),
142-
Length(min=8, message="Password must be at least 8 characters"),
155+
Length(
156+
min=8,
157+
message="Password must be at least 8 characters",
158+
),
143159
],
144160
description="Must be at least 8 characters",
145161
)
@@ -170,7 +186,10 @@ class KitchenSinkForm(FlaskForm):
170186
email_field = StringField(
171187
"EmailField",
172188
widget=GovTextInput(),
173-
validators=[InputRequired(message="EmailField is required"), Email()],
189+
validators=[
190+
InputRequired(message="EmailField is required"),
191+
Email(),
192+
],
174193
description="StringField rendered using a GovTextInput widget.",
175194
)
176195

@@ -207,7 +226,10 @@ class KitchenSinkForm(FlaskForm):
207226
widget=GovCharacterCount(),
208227
validators=[
209228
InputRequired(message="CharacterCountField is required"),
210-
Length(max=200, message="CharacterCountField must be 200 characters or fewer "),
229+
Length(
230+
max=200,
231+
message="CharacterCountField must be 200 characters or fewer ",
232+
),
211233
],
212234
description="TextAreaField rendered using a GovCharacterCount widget.",
213235
)
@@ -237,15 +259,23 @@ class KitchenSinkForm(FlaskForm):
237259
"SelectMultipleField",
238260
widget=GovCheckboxesInput(),
239261
validators=[InputRequired(message="Please select an option")],
240-
choices=[("one", "One"), ("two", "Two"), ("three", "Three")],
262+
choices=[
263+
("one", "One"),
264+
("two", "Two"),
265+
("three", "Three"),
266+
],
241267
description="SelectMultipleField rendered using a GovCheckboxesInput widget.",
242268
)
243269

244270
radio_field = RadioField(
245271
"RadioField",
246272
widget=GovRadioInput(),
247273
validators=[InputRequired(message="Please select an option")],
248-
choices=[("one", "One"), ("two", "Two"), ("three", "Three")],
274+
choices=[
275+
("one", "One"),
276+
("two", "Two"),
277+
("three", "Three"),
278+
],
249279
description="RadioField rendered using a GovRadioInput widget.",
250280
)
251281

@@ -268,10 +298,6 @@ class KitchenSinkForm(FlaskForm):
268298
widget=GovPasswordInput(),
269299
validators=[
270300
InputRequired("Password is required"),
271-
EqualTo(
272-
"password_retype_field",
273-
message="Please ensure both password fields match",
274-
),
275301
],
276302
description="PasswordField rendered using a GovPasswordInput widget.",
277303
)

0 commit comments

Comments
 (0)