@@ -11,4 +11,5 @@ RABBITMQ_DEFAULT_PASS=guest
#EMAIL_PASSWORD=<set this here if needed>
#EMAIL_USE_SSL=<set this here if needed>
#EMAIL_USE_TLS=<set this here if needed>
+#CSRF_TRUSTED_ORIGINS=<set this if the site is not served on https://HOSTNAME>
DEBUG=1
@@ -314,7 +314,9 @@ TOOLS_LOG_DIR = ""
USE_X_FORWARDED_HOST = True
ALLOWED_HOSTS = [os.getenv('HOSTNAME', 'layers.test')]
-CSRF_TRUSTED_ORIGINS = ['https://' + os.getenv('HOSTNAME', 'layers.test')]
+# Comma-separated; set it when the site is not served on https://HOSTNAME,
+# e.g. on a non-standard port
+CSRF_TRUSTED_ORIGINS = (os.getenv('CSRF_TRUSTED_ORIGINS') or 'https://' + os.getenv('HOSTNAME', 'layers.test')).split(',')
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
@@ -582,7 +582,7 @@ def write_env_file(app_dir):
# Write the generated secrets and email settings to .env.local.<service>,
# starting from the tracked .env.dev.<service> defaults. These files hold
# secrets and are ignored by git.
-def write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, email_host, email_port, email_user, email_password, email_ssl, email_tls):
+def write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, email_host, email_port, email_user, email_password, email_ssl, email_tls, site_origin=None):
values = {
'SECRET_KEY': secretkey,
'DATABASE_USER': 'layers',
@@ -596,6 +596,7 @@ def write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, ema
'EMAIL_PASSWORD': email_password,
'EMAIL_USE_SSL': str(email_ssl) if email_ssl else None,
'EMAIL_USE_TLS': str(email_tls) if email_tls else None,
+ 'CSRF_TRUSTED_ORIGINS': site_origin,
'DEBUG': '0',
}
for service in ('app', 'celery', 'db', 'rabbit'):
@@ -613,6 +614,17 @@ def write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, ema
f.write(''.join(newlines))
+# Return the protocol and host[:port] that the site is served on
+def site_protocol_host(hostname, http_port, https_port, no_https):
+ if https_port and not no_https:
+ protocol, port, defport = 'https', https_port, '443'
+ else:
+ protocol, port, defport = 'http', http_port, '80'
+ if port == defport:
+ return protocol, hostname
+ return protocol, '%s:%s' % (hostname, port)
+
+
def check_connectivity(app_dir='/opt'):
return_code = subprocess.call(compose('run', '--rm', 'layersapp', app_dir + '/connectivity_check.sh'), shell=False)
if return_code != 0:
@@ -813,7 +825,8 @@ provide information as needed.
edit_dockerfile(args.http_proxy, args.https_proxy, args.no_proxy)
write_env_file(args.app_dir)
- write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, email_host, email_port, args.email_user, args.email_password, args.email_ssl, args.email_tls)
+ protocol, host = site_protocol_host(args.hostname, http_port, https_port, args.no_https)
+ write_service_env_files(dbpassword, dbapassword, secretkey, rmqpassword, email_host, email_port, args.email_user, args.email_password, args.email_ssl, args.email_tls, '%s://%s' % (protocol, host))
edit_dockercompose(args.hostname, args.portmapping, args.letsencrypt, args.app_dir)
edit_dockerfile_web(args.hostname, args.no_https)
@@ -940,18 +953,7 @@ provide information as needed.
print("Collecting static files failed")
sys.exit(1)
- if https_port and not args.no_https:
- protocol = 'https'
- port = https_port
- defport = '443'
- else:
- protocol = 'http'
- port = http_port
- defport = '80'
- if port == defport:
- host = args.hostname
- else:
- host = '%s:%s' % (args.hostname, port)
+ protocol, host = site_protocol_host(args.hostname, http_port, https_port, args.no_https)
if not args.update:
if not args.databasefile:
@@ -77,10 +77,29 @@ def test_env_files_are_private_and_leave_unset_email_commented(workdir):
app = read_env(workdir / '.env.local.app')
assert 'EMAIL_HOST' not in app
assert 'EMAIL_USE_TLS' not in app
+ assert 'CSRF_TRUSTED_ORIGINS' not in app
# The tracked defaults are left alone
assert read_env(workdir / '.env.dev.db')['MYSQL_ROOT_PASSWORD'] == 'testingpw'
+def test_env_files_get_site_origin(workdir):
+ # The default port 8081 is not the standard https port, so the browser
+ # sends it in the Origin header and Django's CSRF check must allow it
+ dockersetup.write_service_env_files('dbpw', 'rootpw', 'sekrit', 'mqpw', None, None,
+ None, None, False, False, 'https://localhost:8081')
+ assert read_env(workdir / '.env.local.app')['CSRF_TRUSTED_ORIGINS'] == 'https://localhost:8081'
+
+
+@pytest.mark.parametrize('http_port, https_port, no_https, expected', [
+ ('8080', '8081', False, ('https', 'localhost:8081')),
+ ('80', '443', False, ('https', 'localhost')),
+ ('8080', None, True, ('http', 'localhost:8080')),
+ ('80', '8081', True, ('http', 'localhost')),
+])
+def test_site_protocol_host(http_port, https_port, no_https, expected):
+ assert dockersetup.site_protocol_host('localhost', http_port, https_port, no_https) == expected
+
+
def load_compose(workdir):
return yaml.safe_load((workdir / 'docker-compose.yml').read_text())
A default dockersetup.py install serves the site on https://localhost:8081, but CSRF_TRUSTED_ORIGINS only allowed https://localhost, so logging in failed with "Origin checking failed" (403). Let CSRF_TRUSTED_ORIGINS be set from the environment, and have dockersetup.py write the URL the site is served on, including any non-standard port, to .env.local.app. [YOCTO #15329] AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling <tim.orling@konsulko.com> --- .env.dev.app | 1 + docker/settings.py | 4 +++- dockersetup.py | 30 ++++++++++++++++-------------- tests/test_dockersetup.py | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 15 deletions(-)