Skip to content
Open
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
3 changes: 3 additions & 0 deletions blank.env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ DB_PORT=5432
SECRET_KEY=<Not Set>
DB_USER=<Not Set>
DB_PASS=<Not Set>
DB_RO_USER=<Not Set>
DB_RO_PASS=<Not Set>
QUERY_SECRET_KEY=<Not Set>

#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
9 changes: 0 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions nginx/confs/mantidreports.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply the above defined query_limit rate limiting allowing short bursts with delays to be queued further

Suggested change
# Rate limiting
limit_req zone=query_limit burst=10;

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;
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess these numbers are quite generous

Suggested change
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;

}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add extra closing } for the new http block

Suggested change
}
}
}

2 changes: 1 addition & 1 deletion web/run_django.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions web/services/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]

Expand Down
44 changes: 44 additions & 0 deletions web/services/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For improved readability

Suggested change
return response.Response(status=401, data="UNAUTHORIZED")
return response.Response(status=status.HTTP_401_UNAUTHORIZED, 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)
Comment on lines +339 to +345
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception handling for db operations

Suggested change
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)
if sql_err:
return response.Response(status=status.HTTP_400_BAD_REQUEST, data=f"Invalid Parameters: {sql_err}")
try:
conn = connections["readonly"]
with conn.cursor() as cur:
cur.execute(sql)
res = cur.fetchall()
except Exception:
return Response({"error": "Query failed"}, status=status.HTTP_400_BAD_REQUEST)
return response.Response(res)



def get_bearer_token(request):
"""
Expect: Authorization: Bearer <token>
"""
auth = request.headers.get("Authorization", "")
if not auth:
return None
parts = auth.split(None, 1) # ["Bearer", "<token>"]
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To work better with --data-urlencode "sql="

Suggested change
return err, val
def get_parameter(request, param):
val = request.POST.get(param)
if val is None or val.strip() == "":
return f"No {param} parameter provided", None
return None, 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,
Expand Down
13 changes: 12 additions & 1 deletion web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down