* writes test for sharing using the API
* fixes bug on frontend form where an exception occured on generating a new API token if no token existed, yet
* adds permission constraint (default group) for using the api in general
* fixes default-group-only behaviour for sharing-API, so users can only add new users and not removing them, as long as they do not have any other group membership like registration or conservation office
* changes 'ksptoken' to 'Ksptoken' to match CGI standard for http header keys
pull/90/head
mpeltriaux 3 years ago
parent b86202ba98
commit 8f40162974

@ -5,4 +5,4 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 21.01.22
"""
KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken"
KSP_TOKEN_HEADER_IDENTIFIER = "Ksptoken"

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""

@ -0,0 +1,150 @@
import json
from django.urls import reverse
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class APIV1SharingTestCase(BaseTestCase):
@classmethod
def setUpTestData(cls):
""" Creates a test file in memory for using in further tests
Returns:
"""
super().setUpTestData()
cls.superuser.get_API_token()
cls.superuser.api_token.is_active = True
cls.superuser.api_token.save()
default_group = cls.groups.get(name=DEFAULT_GROUP)
cls.superuser.groups.add(default_group)
cls.header_data = {
"HTTP_ksptoken": cls.superuser.api_token.token,
}
def _run_share_request(self, url, user_list: list):
data = {
"users": user_list
}
data = json.dumps(data)
response = self.client.put(
url,
data,
**self.header_data
)
return response
def _test_api_sharing(self, obj, url):
""" Generic test for testing sharing of a ShareableObjectMixin object
Args:
obj (ShareableObjectMixin): The object
url (str): The url to be used for a request
Returns:
"""
self.assertEqual(obj.users.count(), 0)
user_list = [
self.superuser.username,
self.user.username,
]
response = self._run_share_request(url, user_list)
# Must fail, since performing user has no access on requested object
self.assertEqual(response.status_code, 500)
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
# Add performing user to shared access users and rerun the request
obj.users.add(self.superuser)
response = self._run_share_request(url, user_list)
shared_users = obj.shared_users
self.assertEqual(response.status_code, 200)
self.assertEqual(shared_users.count(), 2)
self.assertIn(self.superuser, shared_users)
self.assertIn(self.user, shared_users)
def test_api_token_invalid(self):
""" Tests that a request with an invalid token won't be successfull
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Expect the first request to work properly
self.intervention.users.add(self.superuser)
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
# Change the token
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
# Expect the request to fail now
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 403)
def test_api_intervention_sharing(self):
""" Tests proper sharing of intervention
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
self._test_api_sharing(self.intervention, share_url)
def test_api_eco_account_sharing(self):
""" Tests proper sharing of eco account
Returns:
"""
share_url = reverse("api:v1:ecoaccount-share", args=(self.eco_account.id,))
self._test_api_sharing(self.eco_account, share_url)
def test_api_ema_sharing(self):
""" Tests proper sharing of ema
Returns:
"""
share_url = reverse("api:v1:ema-share", args=(self.ema.id,))
self._test_api_sharing(self.ema, share_url)
def test_api_sharing_as_default_group_only(self):
""" Tests that sharing using the API as an only default group user works as expected.
Expected:
Default only user can only add new users, having shared access. Removing them from the list of users
having shared access is only possible if the user has further rights, e.g. being part of a registration
or conservation office group.
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
self.assertTrue(is_default_group_only(self.superuser))
# Add only him as shared_users an object
self.intervention.users.set([self.superuser])
self.assertEqual(self.intervention.users.count(), 1)
# Try to add another user via API -> must work!
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)
# Now try to remove the user again -> expect no changes at all to the shared user list
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)

@ -18,6 +18,7 @@ from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User
@ -39,6 +40,8 @@ class AbstractAPIView(View):
try:
# Fetch the proper user from the given request header token
self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None))
if not self.user.is_default_user():
raise PermissionError("Default permissions required")
except PermissionError as e:
return self.return_error_response(e, 403)
return super().dispatch(request, *args, **kwargs)
@ -240,6 +243,15 @@ class AbstractModelShareAPIView(AbstractAPIView):
new_users_objs = []
for user in new_users:
new_users_objs.append(User.objects.get(username=user))
if is_default_group_only(self.user):
# Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter(
username__in=new_users
).exclude(
id__in=obj.shared_users
)
new_users_objs = obj.shared_users.union(new_users_to_be_added)
obj.share_with_list(new_users_objs)
return True

@ -7,6 +7,7 @@ Created on: 26.10.21
"""
import datetime
from ema.models import Ema
from user.models import User
from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon
@ -52,6 +53,7 @@ class BaseTestCase(TestCase):
cls.intervention = cls.create_dummy_intervention()
cls.compensation = cls.create_dummy_compensation()
cls.eco_account = cls.create_dummy_eco_account()
cls.ema = cls.create_dummy_ema()
cls.create_dummy_states()
cls.create_dummy_action()
cls.codes = cls.create_dummy_codes()
@ -168,6 +170,30 @@ class BaseTestCase(TestCase):
)
return eco_account
@classmethod
def create_dummy_ema(cls):
""" Creates an ema which can be used for tests
Returns:
"""
# Create dummy data
# Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser)
geometry = Geometry.objects.create()
# Create responsible data object
responsible_data = Responsibility.objects.create()
# Finally create main object, holding the other objects
ema = Ema.objects.create(
identifier="TEST",
title="Test_title",
responsible=responsible_data,
created=action,
geometry=geometry,
comment="Test",
)
return ema
@classmethod
def create_dummy_states(cls):
""" Creates an intervention which can be used for tests

@ -152,6 +152,7 @@ class UserAPITokenForm(BaseForm):
"""
user = self.instance
new_token = self.cleaned_data["token"]
if user.api_token is not None:
user.api_token.delete()
new_token = APIUserToken.objects.create(
token=new_token

@ -12,7 +12,9 @@
</tr>
<tr>
<th scope="row">{% trans 'Authenticated by admins' %}</th>
{% if user.api_token.is_active %}
{% if user.api_token is None %}
<td></td>
{% elif user.api_token.is_active %}
<td class="text-success" title="{% trans 'Token has been verified and can be used' %}">{% fa5_icon 'check-circle' %}</td>
{% else %}
<td class="text-primary" title="{% trans 'Token waiting for verification' %}">{% fa5_icon 'hourglass-half' %}</td>

Loading…
Cancel
Save