-
Notifications
You must be signed in to change notification settings - Fork 1
Add query endpoint for trusted users #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7d506aa
1097166
6d45602
2105678
719ed0c
4bb05c5
7523741
f049303
dde5bff
46b086c
430b52e
d6f45a2
a0e0d56
5399c6c
3adeb69
051353a
6767f12
1811176
a7948ea
0880502
96974d5
3643dc8
2c1b1d4
7bbb564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| # Create your views here. | ||
| from django.views.decorators.cache import cache_page | ||
| from services.models import Message, Usage, FeatureUsage, Location | ||
| from rest_framework import response, viewsets | ||
| from rest_framework import response, viewsets, status | ||
| from rest_framework.decorators import api_view | ||
| from rest_framework.permissions import IsAuthenticatedOrReadOnly, AllowAny | ||
| from services.serializer import ( | ||
|
|
@@ -15,10 +15,17 @@ | |
| 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 | ||
| from hmac import compare_digest | ||
| import logging | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| OS_NAMES = ["Linux", "Windows NT", "Darwin"] | ||
| UTC = datetime.tzinfo("UTC") | ||
|
|
@@ -328,6 +335,71 @@ def by_root(request, format=None): | |
| ) | ||
|
|
||
|
|
||
| @api_view(("POST",)) | ||
| def query(request, format=None): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add an if statement for safeguarding the SQL command. Only SELECT queries are needed, anything else (DROP, DELETE, ALTER...) are unnecessary so they could just get ignored. Even with the restriction of trusted users there could be an innocent mistake.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already protected by the DB due to the read only user account associated with the queries who can only submit select queries. That extra layer of validation can be added to the Django's web service level to free up the DB from it. But still the effect would be equivalent I think. |
||
| if not verify_token(request): | ||
| logger.warning("Unauthorized query attempt") | ||
| return response.Response( | ||
| status=status.HTTP_401_UNAUTHORIZED, data="UNAUTHORIZED" | ||
| ) | ||
|
|
||
| param_err, sql = get_parameter(request, "sql") | ||
| if param_err: | ||
| logger.warning(f"Invalid query parameters: {param_err}") | ||
| return response.Response( | ||
| status=status.HTTP_400_BAD_REQUEST, data=f"Invalid Parameters: {param_err}" | ||
| ) | ||
|
|
||
| if not sql: | ||
| logger.warning("No sql parameter provided") | ||
| return response.Response( | ||
| status=status.HTTP_400_BAD_REQUEST, data="No sql parameter provided" | ||
| ) | ||
|
|
||
| try: | ||
| conn = connections["readonly"] | ||
| with conn.cursor() as cur: | ||
| cur.execute(sql) | ||
| res = cur.fetchall() | ||
| return response.Response(res) | ||
| except Exception: | ||
| logger.exception("Query execution failed") | ||
| return response.Response( | ||
| {"error": "Query failed"}, status=status.HTTP_400_BAD_REQUEST | ||
| ) | ||
|
|
||
|
|
||
| def get_bearer_token(request): | ||
| """ | ||
| Expect: Authorization: Bearer <token> | ||
| """ | ||
| auth = request.headers.get("Authorization", "") | ||
| if not auth: | ||
| logger.warning("No Authorization header provided") | ||
| return None | ||
| parts = auth.split(None, 1) # ["Bearer", "<token>"] | ||
| if len(parts) != 2 or parts[0].lower() != "bearer": | ||
| logger.warning("Invalid Authorization header format") | ||
| return None | ||
| return parts[1].strip() or None | ||
|
|
||
|
|
||
| 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", "") | ||
| if not token or not secret: | ||
| logger.warning("Missing token or secret") | ||
| return False | ||
| return compare_digest(token, secret) | ||
|
|
||
|
|
||
| class FeatureViewSet(viewsets.ModelViewSet): | ||
| """ | ||
| A viewset that provides the standard actions, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.