Skip to content
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
29 changes: 23 additions & 6 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
# Database (all required)
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=5432

# Django
SECRET_KEY=super_secret_key
DJANGO_DEBUG=True # if not specified, defaults to False
ALLOWED_HOSTS=localhost,127.0.0.1 # comma-separated extra hosts appended to production domains

# S3
S3_HOST=https://example.com
S3_KEY=RNAcentral_key
S3_SECRET=RNAcentral_secret
EBI_SEARCH_ENDPOINT=http://example.com # if not specified, www will be used
SECRET_KEY=super_secret_key # if not specified, it uses get_random_secret_key
DJANGO_DEBUG=True # if not specified, it uses DJANGO_DEBUG=False
LOCAL_DEVELOPMENT=True # use this variable if you need the django-debug-toolbar, coverage and mock packages
DOORBELL_API_KEY=your_doorbell_api_key # Doorbell.io API key for feedback integration
DOORBELL_API_ID=your_doorbell_app_id # Doorbell.io application ID

# EBI search
EBI_SEARCH_ENDPOINT=https://www.ebi.ac.uk/ebisearch/ws/rest/rnacentral # prod endpoint

# Doorbell.io feedback
DOORBELL_API_KEY=doorbell_api_key
DOORBELL_API_ID=doorbell_app_id

# Local development only
LOCAL_DEVELOPMENT=True # enables django-debug-toolbar, coverage and mock packages
34 changes: 19 additions & 15 deletions rnacentral/portal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,16 +809,10 @@ def proxy(request):
"This proxy is for www.ebi.ac.uk, wwwdev.ebi.ac.uk, mirbase.org, rfam.org or rna.bgsu.edu only."
)

if domain == "rna.bgsu.edu":
# make sure to use the full url
try:
query_string = request.META["QUERY_STRING"]
url = query_string.split("url=")[1]
except IndexError:
pass

try:
proxied_response = requests.get(url)
# timeout prevents slow-loris / hung connections; allow_redirects=False
# prevents redirect chains from escaping the validated domain.
proxied_response = requests.get(url, timeout=10, allow_redirects=False)
if proxied_response.status_code == 200:
if (
domain == "rfam.org"
Expand Down Expand Up @@ -1084,12 +1078,22 @@ def docbot_feedback(request):

def handler500(request, *args, **argv):
"""
Customized version of handler500 with status_code = 200 in order
to make EBI load balancer to proxy pass to this view, instead of displaying 500.

https://stackoverflow.com/questions/17662928/django-creating-a-custom-500-404-error-page
Custom 500 handler that renders our error template with the correct HTTP status.

The EBI load balancer concern (previously worked around by returning 200) is
already handled at the nginx layer: nginx.yaml intercepts 5xx responses from
Gunicorn via `error_page 500 /error/` and internally proxies to the /error/
Django view (which returns 200), so the load balancer never sees the raw 500.

The load balancer's health probing should use /health-check/ rather than
individual request responses — that endpoint already returns 200/503 based on
real service state and is documented as the Traffic Manager signal.

NOTE FOR INFRASTRUCTURE TEAM: if the EBI load balancer is still configured to
probe arbitrary page responses rather than /health-check/, please update it to
target /health-check/ instead. This restores correct HTTP semantics and ensures
application errors are visible to monitoring without affecting load-balancer
routing decisions.
"""
# warning: in django2 signature of this function has changed
response = render(request, "500.html", {})
response.status_code = 200
return response
52 changes: 44 additions & 8 deletions rnacentral/rnacentral/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,28 @@

MANAGERS = ADMINS

# use the public Postgres database as the default value
def _require_env(name):
value = os.getenv(name)
if not value:
raise RuntimeError(f"Required environment variable '{name}' is not set.")
return value


DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": os.getenv("DB_NAME", "pfmegrnargs"),
"USER": os.getenv("DB_USERNAME", "reader"),
"PASSWORD": os.getenv("DB_PASSWORD", "NWDMCE5xdipIjRrp"),
"HOST": os.getenv("DB_HOST", "hh-pgsql-public.ebi.ac.uk"),
"NAME": _require_env("DB_NAME"),
"USER": _require_env("DB_USER"),
"PASSWORD": _require_env("DB_PASSWORD"),
"HOST": _require_env("DB_HOST"),
"PORT": os.getenv("DB_PORT", 5432),
}
}

# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["*"]

_extra_hosts = [h for h in os.getenv("ALLOWED_HOSTS", "").split(",") if h]
ALLOWED_HOSTS = ["rnacentral.org", "www.rnacentral.org", "test.rnacentral.org"] + _extra_hosts

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
Expand Down Expand Up @@ -113,6 +120,8 @@
)

MIDDLEWARE = (
# SecurityMiddleware must be first so security headers are applied to all responses
"django.middleware.security.SecurityMiddleware",
# gzip
"django.middleware.gzip.GZipMiddleware",
# default
Expand Down Expand Up @@ -160,7 +169,20 @@

USE_ETAGS = True

CORS_ORIGIN_ALLOW_ALL = True
# Restrict CORS to known consumers rather than allowing all origins.
# django-cors-headers evaluates CORS_ALLOWED_ORIGIN_REGEXES when an exact-match
# against CORS_ALLOWED_ORIGINS fails, so both lists are checked.
#
# Add localhost / 127.0.0.1 origins in local_settings.py when running locally:
# CORS_ALLOWED_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]
CORS_ALLOWED_ORIGIN_REGEXES = [
# All RNAcentral subdomains (test, sequence-search, blog, search, …)
r"^https://([a-z0-9-]+\.)*rnacentral\.org$",
# All EBI subdomains (www, wwwdev, wwwint, …)
r"^https://([a-z0-9-]+\.)*ebi\.ac\.uk$",
# GitHub Pages hosting the sequence-search embed widget
r"^https://rnacentral\.github\.io$",
]

ROOT_URLCONF = "rnacentral.urls"

Expand Down Expand Up @@ -373,6 +395,20 @@
# Use a simplified runner to prevent any modifications to the database.
TEST_RUNNER = "portal.tests.test_runner.FixedRunner"

# Security settings (defence in depth — some may also be enforced at nginx/load-balancer level)
# SECURE_BROWSER_XSS_FILTER is intentionally omitted: it was removed in Django 5.0 because
# the X-XSS-Protection header it set is considered harmful on modern browsers.
SECURE_SSL_REDIRECT = not DEBUG
# Tell SecurityMiddleware that HTTPS is indicated by the X-Forwarded-Proto header set by the
# reverse proxy, so that SECURE_SSL_REDIRECT does not loop on proxied requests.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
X_FRAME_OPTIONS = "DENY"

try:
from .local_settings import *
except ImportError:
Expand Down
4 changes: 2 additions & 2 deletions rnacentral/rnacentral/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@
urlpatterns += additional_settings


# Override 500 page, so that in case of an error, we still display our error page with normal response status
# and EBI load balancer still proxies to our website instead of showing an EBI 'service down' page
# Override 500 handler to render our custom error template.
# Load-balancer health probing should target /health-check/, not individual responses.
handler500 = "portal.views.handler500"