diff --git a/catalog/context_processors.py b/catalog/context_processors.py index 3b55ceaf..c35cd832 100644 --- a/catalog/context_processors.py +++ b/catalog/context_processors.py @@ -1,5 +1,7 @@ from django.conf import settings from pgs_web import constants +from pgs_web import external_urls + def pgs_settings(request): return { @@ -8,6 +10,7 @@ def pgs_settings(request): 'is_pgs_curation_site': settings.PGS_ON_CURATION_SITE } + def pgs_urls(request): return { 'nhgri_url' : constants.USEFUL_URLS['NHGRI_URL'], @@ -28,6 +31,17 @@ def pgs_urls(request): 'catalog_publication_url': constants.USEFUL_URLS['CATALOG_PUBLICATION_URL'] } + +def styles_urls(request): + return { + 'bootstrap_style_url': external_urls.STYLES_URLS['bootstrap'], + 'bootstrap_table_style_url': external_urls.STYLES_URLS['bootstrap-table'], + 'font_awesome_style_url': external_urls.STYLES_URLS['font-awesome'], + 'jquery_style_url': external_urls.STYLES_URLS['jquery'], + 'ebi_style_url': external_urls.STYLES_URLS['ebi'], + } + + def pgs_search_examples(request): eg_count = 0 html = '' @@ -46,6 +60,7 @@ def pgs_search_examples(request): 'pgs_search_examples': html } + def pgs_info(request): return { 'pgs_citation': constants.PGS_CITATIONS[0], @@ -55,6 +70,7 @@ def pgs_info(request): 'ensembl_version': constants.ENSEMBL_VERSION } + def pgs_contributors(request): groups = constants.PGS_GROUPS groups_to_print = list() diff --git a/catalog/middleware/add_nonce.py b/catalog/middleware/add_nonce.py new file mode 100644 index 00000000..c5941c68 --- /dev/null +++ b/catalog/middleware/add_nonce.py @@ -0,0 +1,23 @@ +import re + + +class AddNonceToScriptsMiddleware: + """This middleware class adds the CSP nonce to every + - + {% if use_gwas_api %} - + {% endif %} {% if use_release_charts %} - + {% endif %} - + {% if has_table %} - - - - + + + + {% endif %} {% if has_chart %} - + {% endif %} {% if has_chart_js %} - - - - + + + + {% endif %} {% if is_pgs_app_on_gae %} - + {% else %} {% compress js file pgs_min %} - + {% endcompress %} {% endif %} {% if is_benchmark and has_chart %} - - + + {% if is_pgs_app_on_gae %} - + {% else %} {% compress js file benchmark_min %} - + {% endcompress %} {% endif %} {% endif %} diff --git a/curation_tracker/static/curation_tracker/pgs_edit_curation_annotation_addon.js b/curation_tracker/static/curation_tracker/pgs_edit_curation_annotation_addon.js index 024ac202..8aeda057 100644 --- a/curation_tracker/static/curation_tracker/pgs_edit_curation_annotation_addon.js +++ b/curation_tracker/static/curation_tracker/pgs_edit_curation_annotation_addon.js @@ -9,7 +9,8 @@ function setError(error){ } } -function goToPGP(new_window){ +function goToPGP(event){ + var new_window = event.data.new_window; setError(''); var pgp_id = $('#id_pgp_id').val(); if(pgp_id){ @@ -22,6 +23,7 @@ function goToPGP(new_window){ } else { alert('Please provide a PGP ID first'); } + return false; // To avoid reloading the current page } function goToPublication(){ @@ -35,6 +37,7 @@ function goToPublication(){ } else { alert('Please provide a DOI or PubMed ID'); } + return false; // To avoid reloading the current page } function toggleAuthorSub(){ @@ -113,6 +116,7 @@ function autofillForm(){ var doi = $('#id_doi').val(); var pmid = $('#id_PMID').val(); _getPublicationInfo({doi: doi, pmid: pmid}); + return false; // To avoid reloading the current page } function requestAuthorData(){ @@ -141,10 +145,17 @@ $(document).ready(function(){ var pgp_id_div = $('div.form-row.field-pgp_id > div'); pgp_id_div.addClass('flex-container'); pgp_id_div.find(">:first-child").addClass('fieldBox'); - pgp_id_div.append('
Go to PGS Catalog publication
'); + pgp_id_div.append('
Go to PGS Catalog publication
'); // Adding 'go to publication' and 'Autofill' buttons after the DOI and PMID form fields - $('div.form-row.field-doi.field-PMID > div.flex-container').append('
Go to publication
Autofill
    '); + $('div.form-row.field-doi.field-PMID > div.flex-container').append('
    Go to publication
    Autofill
      '); + + // Adding actions to the buttons + var pgp_link_selector = $('#go_to_pgp_link') + pgp_link_selector.click({'new_window': false}, goToPGP); + pgp_link_selector.on('auxclick', {'new_window': true}, goToPGP); + $('#go_to_publication_link').click(goToPublication); + $('#autofill_link').click(autofillForm); // Adding toggle AuthorSub suffix function $('#id_author_submission').click(toggleAuthorSub); diff --git a/pgs_web/external_urls.py b/pgs_web/external_urls.py new file mode 100644 index 00000000..35e0dc7e --- /dev/null +++ b/pgs_web/external_urls.py @@ -0,0 +1,7 @@ +STYLES_URLS = { + 'bootstrap': 'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css', + 'bootstrap-table': 'https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.css', + 'font-awesome': 'https://use.fontawesome.com/releases/v6.7.2/css/all.css', + 'jquery': 'https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css', + 'ebi': 'https://ebi.emblstatic.net/web_guidelines/EBI-Icon-fonts/v1.3/fonts.css' +} \ No newline at end of file diff --git a/pgs_web/settings.py b/pgs_web/settings.py index 545c3b3a..0b4d3390 100644 --- a/pgs_web/settings.py +++ b/pgs_web/settings.py @@ -3,6 +3,16 @@ """ import os +from pgs_web.constants import USEFUL_URLS +from pgs_web.external_urls import STYLES_URLS +from urllib.parse import urlparse + + +def get_base_url(full_url): + parse_result = urlparse(full_url) + base_url = parse_result.scheme + '://' + parse_result.netloc + return base_url + if not os.getenv('GAE_APPLICATION', None): app_settings = os.path.join('./', 'app.yaml') @@ -95,9 +105,59 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware' ] + +# ----------------------------- # +# Content Security Policy (CSP) # +# ----------------------------- # +if not DEBUG: + INSTALLED_APPS.append('csp') + MIDDLEWARE.extend([ + 'csp.middleware.CSPMiddleware', + 'catalog.middleware.add_nonce.AddNonceToScriptsMiddleware' + ]) + +CSP_INCLUDE_NONCE_IN = [ + 'script-src' +] +# default-src +# "strict-dynamic" allows trusted (with nonce) resources to load additional external resources. +CSP_DEFAULT_SRC = ("'self'", "'strict-dynamic'") +# base-uri +CSP_BASE_URI = "'self'" +# frame-ancestors +CSP_FRAME_ANCESTORS = ("'self'", + USEFUL_URLS['EBI_URL'], # Allowing the PGS Catalog to be shown in an EBI training iframe + ) +# style-src +# We can't include the CSP nonce into style-src as the templates contain a lot of inline styles within attributes. +# It is therefore necessary to specify all trusted style sources explicitly, including those called from external resources. +CSP_STYLE_SRC = ("'self'", + "'unsafe-inline'", + *(get_base_url(url) for url in STYLES_URLS.values()) + ) +# script-src +CSP_SCRIPT_SRC = ("'self'", + "'strict-dynamic'", + # The following are only here for backward compatibility with old browsers, as they are unsafe. + # Modern browsers support nonce and "strict-dynamic", which makes them ignore the following. + "'unsafe-inline'", + "http:", + "https:" + ) +# img-src +CSP_IMG_SRC = ("'self'", + "data:" # For SVG images + ) +# front-src +CSP_FONT_SRC = ("'self'", + get_base_url(STYLES_URLS['font-awesome']), + get_base_url(STYLES_URLS['ebi'])) +# connect-src +CSP_CONNECT_SRC = ("'self'", + USEFUL_URLS['EBI_URL']) + # Live middleware if PGS_ON_LIVE_SITE: MIDDLEWARE.insert(2, 'corsheaders.middleware.CorsMiddleware') @@ -115,6 +175,7 @@ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'catalog.context_processors.pgs_urls', + 'catalog.context_processors.styles_urls', 'catalog.context_processors.pgs_settings', 'catalog.context_processors.pgs_search_examples', 'catalog.context_processors.pgs_info', diff --git a/requirements.txt b/requirements.txt index 76c7c229..0fdbd055 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,5 @@ google-auth==2.16.1 google-auth-oauthlib==1.0.0 google-cloud-core==2.3.2 google-cloud-storage==2.7.0 +#### Content Security Policy (CSP) +django-csp==3.8 \ No newline at end of file