Skip to content

Commit

Permalink
Merge pull request #359 from open-contracting/feat/login
Browse files Browse the repository at this point in the history
fix: authorization (docs, migration, code refactor)
  • Loading branch information
mariob0y committed Sep 16, 2021
2 parents 2f4e70e + 158a396 commit 8278e35
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 31 deletions.
23 changes: 23 additions & 0 deletions core/migrations/0042_alter_url_author.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.3 on 2021-09-14 11:07

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("core", "0041_auto_20210827_0732"),
]

operations = [
migrations.AlterField(
model_name="url",
name="author",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
]
34 changes: 17 additions & 17 deletions core/tests/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import pathlib
import shutil
from base64 import b64encode
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -227,10 +228,14 @@ def test_dataregistry_path(self, client, tmp_path):
password = "test"
user = User.objects.create_user(username=username, password=password)
user.save()
credentials = f"{username}:{password}"
encoded_credentials = b64encode(credentials.encode("ascii")).decode("ascii")

# Relative path
url = "file://file.json"
response = client.post(f"{self.url_prefix}", {"urls": url}, HTTP_USERNAME=username, HTTP_PASSWORD=password)
response = client.post(
f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}"
)
path = dataregistry_path_resolver(dataregistry_path_formatter(url))
assert os.path.isfile(file)
assert str(path) == str(file)
Expand All @@ -242,7 +247,9 @@ def test_dataregistry_path(self, client, tmp_path):

# Absolute path
url = "file://" + str(file)
response = client.post(f"{self.url_prefix}", {"urls": url}, HTTP_USERNAME=username, HTTP_PASSWORD=password)
response = client.post(
f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}"
)
path = dataregistry_path_resolver(dataregistry_path_formatter(url))
assert str(path) == str(file)
assert response.status_code == 201
Expand All @@ -255,48 +262,43 @@ def test_dataregistry_path(self, client, tmp_path):
assert str(path) == str(forbidden_file)
assert os.path.isfile(forbidden_file)
with pytest.raises(ValueError) as e:
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_USERNAME=username, HTTP_PASSWORD=password)
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")
assert "Input URL is invalid" in str(e)

# Path that leads outside of data registry folder
url = "file://" + str(tmp_path) + "/data_registry/../forbidden_file.json"
path = dataregistry_path_resolver(dataregistry_path_formatter(url))
assert str(path) == str(tmp_path) + "/forbidden_file.json"
with pytest.raises(ValueError) as e:
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_USERNAME=username, HTTP_PASSWORD=password)
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")
assert "Input URL is invalid" in str(e)

# Path that leads to root
url = "file:///./../../../../../../../../forbidden_file.json"
path = dataregistry_path_resolver(dataregistry_path_formatter(url))
assert str(path) == "/forbidden_file.json"
with pytest.raises(ValueError) as e:
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_USERNAME=username, HTTP_PASSWORD=password)
assert "Input URL is invalid" in str(e)
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")

# Symlink not allowed
dest = tmp_path / "data_registry/forbidden_file.json"
os.symlink(forbidden_file, str(dest))
assert os.path.islink(dest)
url = "file://" + str(tmp_path) + "/data_registry" + "/forbidden_file.json"
with pytest.raises(ValueError) as e:
client.post(f"{self.url_prefix}", {"urls": url}, headers={"username": username, "password": password})
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")
assert "Input URL is invalid" in str(e)

# Symlink allowed, jail is on
with patch("core.validators.settings.DATAREGISTRY_ALLOW_SYMLINKS", True):
with pytest.raises(ValueError) as e:
client.post(
f"{self.url_prefix}", {"urls": url}, headers={"HTTP_USERNAME": username, "password": password}
)
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")
assert "Input URL is invalid" in str(e)

# Symlink allowed, jail is off
with patch("core.validators.settings.DATAREGISTRY_ALLOW_SYMLINKS", True):
with patch("core.validators.settings.DATAREGISTRY_JAIL", False):
client.post(
f"{self.url_prefix}", {"urls": url}, headers={"HTTP_USERNAME": username, "password": password}
)
client.post(f"{self.url_prefix}", {"urls": url}, HTTP_AUTHORIZATION=f"Basic {encoded_credentials}")
assert response.status_code == 201

# Multi upload dataregistry path creation successful
Expand All @@ -312,8 +314,7 @@ def test_dataregistry_path(self, client, tmp_path):
f"{self.url_prefix}",
json.dumps({"urls": paths}),
content_type="application/json",
HTTP_USERNAME=username,
HTTP_PASSWORD=password,
HTTP_AUTHORIZATION=f"Basic {encoded_credentials}",
)
url = response.json()
assert response.status_code == 201
Expand All @@ -326,8 +327,7 @@ def test_dataregistry_path(self, client, tmp_path):
f"{self.url_prefix}",
json.dumps({"urls": paths}),
content_type="application/json",
HTTP_USERNAME=username,
HTTP_PASSWORD=password,
HTTP_AUTHORIZATION=f"Basic {encoded_credentials}",
)
assert "Multiple uploads are not available for this type of URL" in response.json()["detail"]["urls"]

Expand Down
39 changes: 25 additions & 14 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from datetime import timedelta

from django.conf import settings
from django.contrib.auth import authenticate
from django.core.exceptions import ValidationError
from django.core.files import File
from django.core.files.base import ContentFile
Expand Down Expand Up @@ -221,9 +220,24 @@ class URLViewSet(viewsets.GenericViewSet):
}
```
## **Authorization**
Dataregistry paths usage is allowed only to authorized users. In order to send authorized request - header should include following details: \n
`"username": <USERNAME>` \n
`"password": <PASSWORD>` \n
Only authorized users are allowed to use file URIs (ones that starts with `file://`).
User's request may be authorized through HTTP Basic Authorization.
In order to send authorized request - HTTP header should include 'Authorization' field; and base64-encoded credentials: \n
`"Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="` \n
Please note, that user's credentials are regular username and password, but those should be encoded before sending a request.
Example of encoding credentials you may see below
### **Example of authorized request with encoded credentials (python script)**
```python
import requests
username = 'johhsmith'
password = 'youshallnotpass'
response = requests.post('/urls/',
{'urls': ["file://document.json"]},
auth=(username, password))
print(response.json())
```
Through terminal commands you may create, delete or edit users in database.
### **Creating new users**
Expand Down Expand Up @@ -265,9 +279,6 @@ def retrieve(self, request, id=None, *args, **kwargs):
return Response(serializer.data, status=status.HTTP_200_OK)

def create(self, request, *args, **kwargs):
username = request.META.get("HTTP_USERNAME", "")
password = request.META.get("HTTP_PASSWORD", "")
user = authenticate(request, username=username, password=password)
try:
urls = request.POST.get("urls", "") or request.data.get("urls", "")
if not urls:
Expand All @@ -278,14 +289,14 @@ def create(self, request, *args, **kwargs):
validation_obj = Validation.objects.create()
url_obj = Url.objects.create(**serializer.validated_data)
protocol = get_protocol(serializer.validated_data["urls"][0])
if protocol == "file":
if user:
url_obj.author = user
url_obj.save(update_fields=["author"])
else:
return Response({"detail": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
if request.user.is_authenticated:
url_obj.author = request.user
elif protocol == "file":
return Response({"detail": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
else:
url_obj.author = None
url_obj.validation = validation_obj
url_obj.save(update_fields=["validation"])
url_obj.save(update_fields=["validation", "author"])
lang_code = get_language()
download_data_source.delay(url_obj.id, model="Url", lang_code=lang_code)
return Response(self.get_serializer_class()(url_obj).data, status=status.HTTP_201_CREATED)
Expand Down
8 changes: 8 additions & 0 deletions spoonbill_web/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,11 @@
DATAREGISTRY_MEDIA_ROOT = os.getenv("DATAREGISTRY_MEDIA_ROOT", None)
if DATAREGISTRY_MEDIA_ROOT:
DATAREGISTRY_MEDIA_ROOT = Path(DATAREGISTRY_MEDIA_ROOT)


REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.BasicAuthentication",),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
}

0 comments on commit 8278e35

Please sign in to comment.