diff --git a/blank.env b/blank.env index df82549..0f8f6aa 100644 --- a/blank.env +++ b/blank.env @@ -17,6 +17,9 @@ DB_PORT=5432 SECRET_KEY= DB_USER= DB_PASS= +DB_RO_USER= +DB_RO_PASS= +QUERY_SECRET_KEY= #Trusted origins for CSRF validation. For production usage only specify https://reports.mantidproject.org DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8082,https://reports.a.staging-mantidproject.stfc.ac.uk diff --git a/docker-compose.yml b/docker-compose.yml index af74954..7630158 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,9 +10,6 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} - DB_SERVICE: ${DB_SERVICE} - DB_PORT: ${DB_PORT} - SECRET_KEY: ${SECRET_KEY} adminer: image: adminer @@ -34,12 +31,6 @@ services: depends_on: - postgres env_file: .env - environment: - DB_SERVICE: ${DB_SERVICE} - DB_PORT: ${DB_PORT} - SECRET_KEY: ${SECRET_KEY} - # Define this in .env for development mode. DO NOT USE IN PRODUCTION - DEBUG: ${DEBUG} nginx-reports: restart: always diff --git a/nginx/confs/mantidreports.conf b/nginx/confs/mantidreports.conf index ef494e9..ce9c645 100644 --- a/nginx/confs/mantidreports.conf +++ b/nginx/confs/mantidreports.conf @@ -32,4 +32,23 @@ server { proxy_read_timeout 60s; } + set_real_ip_from 172.0.17.0/24; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + location /api/query { + # allow ISIS VPN traffic + allow 130.246.0.0/16; + deny all; + + proxy_pass http://web:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } } diff --git a/web/run_django.sh b/web/run_django.sh index 5a52ae3..b36ddb8 100755 --- a/web/run_django.sh +++ b/web/run_django.sh @@ -8,7 +8,7 @@ python manage.py migrate --noinput # If running in DEBUG mode add debug logging to gunicorn if [ -n "${DEBUG}" ]; then - DEBUG_ARGS="--log-level debug --capture-output" + DEBUG_ARGS="--log-level debug --capture-output --reload" else DEBUG_ARGS= fi diff --git a/web/services/urls.py b/web/services/urls.py index 66cfb40..c7075c9 100644 --- a/web/services/urls.py +++ b/web/services/urls.py @@ -17,6 +17,7 @@ path("by/user", views.usage_by_users, name="by-users"), path("host", views.host_list, name="host-list"), path("user", views.user_list, name="user-list"), + path("query", views.query, name="query"), # url(r'feature', views.feature_usage, name='feature_usage'), ] diff --git a/web/services/views.py b/web/services/views.py index ef8e81e..5fa75d6 100644 --- a/web/services/views.py +++ b/web/services/views.py @@ -15,10 +15,13 @@ import django_filters from rest_framework.reverse import reverse from django.http import HttpResponse +from django.db import connections + import json import datetime import hashlib import services.plots as plotsfile +from os import environ OS_NAMES = ["Linux", "Windows NT", "Darwin"] UTC = datetime.tzinfo("UTC") @@ -328,6 +331,47 @@ def by_root(request, format=None): ) +@api_view(("POST",)) +def query(request, format=None): + if not verify_token(request): + return response.Response(status=401, data="UNAUTHORIZED") + sql_err, sql = get_parameter(request, "sql") + if sql_err: + return response.Response(status=400, data=f"Invalid Parameters: {sql_err}") + conn = connections["readonly"] + with conn.cursor() as cur: + cur.execute(sql) + res = cur.fetchall() + return response.Response(res) + + +def get_bearer_token(request): + """ + Expect: Authorization: Bearer + """ + auth = request.headers.get("Authorization", "") + if not auth: + return None + parts = auth.split(None, 1) # ["Bearer", ""] + if len(parts) != 2 or parts[0].lower() != "bearer": + return None + return parts[1].strip() or None + + +def get_parameter(request, param): + val = request.POST.get(param) + err = "" + if not val: + err = f"No {param} parameter provided" + return err, val + + +def verify_token(request) -> bool: + token = get_bearer_token(request) + secret = environ.get("QUERY_SECRET_KEY", "") + return token == secret + + class FeatureViewSet(viewsets.ModelViewSet): """ A viewset that provides the standard actions, diff --git a/web/settings.py b/web/settings.py index a5b2213..34d7bab 100644 --- a/web/settings.py +++ b/web/settings.py @@ -94,7 +94,18 @@ "PASSWORD": os.environ["DB_PASS"], "HOST": os.environ["DB_SERVICE"], "PORT": os.environ["DB_PORT"], - } + }, + "readonly": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_RO_USER"], + "PASSWORD": os.environ["DB_RO_PASS"], + "HOST": os.environ["DB_SERVICE"], + "PORT": os.environ["DB_PORT"], + "OPTIONS": { + "options": "-c default_transaction_read_only=on", + }, + }, } # Internationalization