Skip to content
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

feat(organizations): add new asset endpoint for owners and admins TASK-960 #5218

Merged
merged 29 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cccb2c9
Draft of organization assets endpoint
noliveleger Oct 21, 2024
dffa612
Merge branch 'task-958-set-owner-accordingly' into task-960-organizat…
noliveleger Oct 21, 2024
9c7f4b2
Add new endpoint for org assets
noliveleger Oct 22, 2024
857867e
Restrict org admin to view org assets only
noliveleger Oct 22, 2024
bce9c54
Handle permission exception for org admins
noliveleger Oct 23, 2024
7b312d4
Add delete asset permission to org admins
noliveleger Oct 23, 2024
a649509
Merge branch 'main' into task-960-organization-assets-endpoint
noliveleger Oct 24, 2024
914f102
Merge branch 'task-958-set-owner-accordingly' into task-960-organizat…
noliveleger Oct 24, 2024
05efe40
Make org admins access project details
noliveleger Oct 24, 2024
8c6fff3
Merge branch 'task-958-set-owner-accordingly' into task-960-organizat…
noliveleger Oct 25, 2024
f06fd5b
Merge branch 'task-958-set-owner-accordingly' into task-960-organizat…
noliveleger Oct 25, 2024
e529991
Merge branch 'main' into task-960-organization-assets-endpoint
noliveleger Oct 28, 2024
88a289b
Fix bad merge
noliveleger Oct 29, 2024
d9c8430
Bypass Guardian in OpenRosa when user is an org admin
noliveleger Oct 29, 2024
5800ed4
Rename variables
noliveleger Oct 29, 2024
e8227b7
Delete own organization when user is added to another org
noliveleger Oct 29, 2024
1eaa215
Block add_user if org is not MMO
noliveleger Oct 29, 2024
6b3ac7d
Refactor permissions system for org admins on related asset objects
noliveleger Oct 30, 2024
38482b1
Refactor old unit tests for organization admins tests
noliveleger Oct 30, 2024
edc46a1
Refactor AssetFile and AssetSnapshot permission and filter mechanism …
noliveleger Oct 30, 2024
32cc65a
Fix add_user() with multi-member orgs
noliveleger Oct 30, 2024
2a80fe3
Fix organization name being empty on sync
noliveleger Oct 31, 2024
6ef682d
Merge branch 'main' into task-960-organization-assets-endpoint
noliveleger Oct 31, 2024
4339820
Linting
noliveleger Oct 31, 2024
58163fd
Refactoring unit tests to use common code for adding, editing and del…
noliveleger Oct 31, 2024
f51bdfe
Fix 0006 db migrations to avoid overwriting org name with empty strings
noliveleger Nov 4, 2024
ee4f9ae
Merge branch 'main' into task-960-organization-assets-endpoint
magicznyleszek Nov 7, 2024
d67f2af
Merge branch 'main' into task-960-organization-assets-endpoint
magicznyleszek Nov 11, 2024
3317899
Merge branch 'main' into task-960-organization-assets-endpoint
magicznyleszek Nov 11, 2024
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
5 changes: 3 additions & 2 deletions hub/models/extra_user_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,6 @@ def _sync_org_name(self):
except (KeyError, AttributeError):
organization_name = None

user_organization.name = organization_name
user_organization.save(update_fields=['name'])
if organization_name:
user_organization.name = organization_name
user_organization.save(update_fields=['name'])
125 changes: 4 additions & 121 deletions kobo/apps/hook/tests/hook_test_case.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
# coding: utf-8
import json
import uuid

import pytest
import responses
from django.conf import settings
from django.urls import reverse
from rest_framework import status

from kpi.constants import SUBMISSION_FORMAT_TYPE_JSON, SUBMISSION_FORMAT_TYPE_XML
from kpi.exceptions import BadFormatException
from kpi.tests.kpi_test_case import KpiTestCase
from ..constants import HOOK_LOG_FAILED
from ..exceptions import HookRemoteServerDownError
from ..models import Hook, HookLog
from ..models import Hook
from ..utils.tests.mixins import HookTestCaseMixin


class HookTestCase(KpiTestCase):
class HookTestCase(HookTestCaseMixin, KpiTestCase):

def setUp(self):
self.client.login(username='someuser', password='someuser')
Expand Down Expand Up @@ -49,115 +39,8 @@ def setUp(self):
self.asset.deploy(backend='mock', active=True)
self.asset.save()
self.hook = Hook()
self._submission_pk = 1

settings.CELERY_TASK_ALWAYS_EAGER = True

def _create_hook(self, return_response_only=False, **kwargs):

format_type = kwargs.get('format_type', SUBMISSION_FORMAT_TYPE_JSON)
if format_type not in [
SUBMISSION_FORMAT_TYPE_JSON,
SUBMISSION_FORMAT_TYPE_XML,
]:
raise BadFormatException(
'The format {} is not supported'.format(format_type)
)

self.__prepare_submission()

url = reverse('hook-list', args=(self.asset.uid,))
data = {
'name': kwargs.get('name', 'some external service with token'),
'endpoint': kwargs.get('endpoint', 'http://external.service.local/'),
'settings': kwargs.get('settings', {
'custom_headers': {
'X-Token': '1234abcd'
}
}),
'export_type': format_type,
'active': kwargs.get('active', True),
'subset_fields': kwargs.get('subset_fields', []),
'payload_template': kwargs.get('payload_template', None)
}

response = self.client.post(url, data, format='json')
if return_response_only:
return response
else:
self.assertEqual(
response.status_code, status.HTTP_201_CREATED, msg=response.data
)
hook = self.asset.hooks.last()
self.assertTrue(hook.active)
return hook

def _send_and_fail(self):
"""
The public method which calls this method needs to be decorated by
`@responses.activate`

:return: dict
"""
first_hooklog_response = self._send_and_wait_for_retry()

# Fakes Celery n retries by forcing status to `failed`
# (where n is `settings.HOOKLOG_MAX_RETRIES`)
first_hooklog = HookLog.objects.get(uid=first_hooklog_response.get('uid'))
first_hooklog.change_status(HOOK_LOG_FAILED)

return first_hooklog_response

def _send_and_wait_for_retry(self):
self.hook = self._create_hook()

ServiceDefinition = self.hook.get_service_definition()
submissions = self.asset.deployment.get_submissions(self.asset.owner)
submission_id = submissions[0]['_id']
service_definition = ServiceDefinition(self.hook, submission_id)
first_mock_response = {'error': 'gateway timeout'}

# Mock first request's try
responses.add(
responses.POST,
self.hook.endpoint,
json=first_mock_response,
status=status.HTTP_504_GATEWAY_TIMEOUT,
)

# Mock next requests' tries
responses.add(
responses.POST,
self.hook.endpoint,
status=status.HTTP_200_OK,
content_type='application/json',
)

# Try to send data to external endpoint
with pytest.raises(HookRemoteServerDownError):
service_definition.send()

# Retrieve the corresponding log
url = reverse('hook-log-list', kwargs={
'parent_lookup_asset': self.hook.asset.uid,
'parent_lookup_hook': self.hook.uid
})

response = self.client.get(url)
first_hooklog_response = response.data.get('results')[0]

# Result should match first try
self.assertEqual(
first_hooklog_response.get('status_code'),
status.HTTP_504_GATEWAY_TIMEOUT,
)
self.assertEqual(
json.loads(first_hooklog_response.get('message')),
first_mock_response,
)
return first_hooklog_response

def __prepare_submission(self):
def _add_submissions(self):
v_uid = self.asset.latest_deployed_version.uid
self.submission = {
'__version__': v_uid,
Expand Down
Empty file.
118 changes: 118 additions & 0 deletions kobo/apps/hook/utils/tests/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import json

import pytest
import responses
from django.urls import reverse
from rest_framework import status

from kpi.constants import SUBMISSION_FORMAT_TYPE_JSON, SUBMISSION_FORMAT_TYPE_XML
from kpi.exceptions import BadFormatException
from kobo.apps.hook.constants import HOOK_LOG_FAILED
from kobo.apps.hook.exceptions import HookRemoteServerDownError
from kobo.apps.hook.models import HookLog


class HookTestCaseMixin:

def _create_hook(self, return_response_only=False, **kwargs):

format_type = kwargs.get('format_type', SUBMISSION_FORMAT_TYPE_JSON)
if format_type not in [
SUBMISSION_FORMAT_TYPE_JSON,
SUBMISSION_FORMAT_TYPE_XML,
]:
raise BadFormatException(
'The format {} is not supported'.format(format_type)
)

self._add_submissions()

url = reverse('hook-list', args=(self.asset.uid,))
data = {
'name': kwargs.get('name', 'some external service with token'),
'endpoint': kwargs.get('endpoint', 'http://external.service.local/'),
'settings': kwargs.get('settings', {
'custom_headers': {
'X-Token': '1234abcd'
}
}),
'export_type': format_type,
'active': kwargs.get('active', True),
'subset_fields': kwargs.get('subset_fields', []),
'payload_template': kwargs.get('payload_template', None)
}

response = self.client.post(url, data, format='json')
if return_response_only:
return response
else:
self.assertEqual(
response.status_code, status.HTTP_201_CREATED, msg=response.data
)
hook = self.asset.hooks.last()
self.assertTrue(hook.active)
return hook

def _send_and_fail(self) -> dict:
"""
The public method which calls this method needs to be decorated by
`@responses.activate`
"""

first_hooklog_response = self._send_and_wait_for_retry()

# Fakes Celery n retries by forcing status to `failed`
# (where n is `settings.HOOKLOG_MAX_RETRIES`)
first_hooklog = HookLog.objects.get(uid=first_hooklog_response.get('uid'))
first_hooklog.change_status(HOOK_LOG_FAILED)

return first_hooklog_response

def _send_and_wait_for_retry(self):
self.hook = self._create_hook()

ServiceDefinition = self.hook.get_service_definition()
submissions = self.asset.deployment.get_submissions(self.asset.owner)
submission_id = submissions[0]['_id']
service_definition = ServiceDefinition(self.hook, submission_id)
first_mock_response = {'error': 'gateway timeout'}

# Mock first request's try
responses.add(
responses.POST,
self.hook.endpoint,
json=first_mock_response,
status=status.HTTP_504_GATEWAY_TIMEOUT,
)

# Mock next requests' tries
responses.add(
responses.POST,
self.hook.endpoint,
status=status.HTTP_200_OK,
content_type='application/json',
)

# Try to send data to external endpoint
with pytest.raises(HookRemoteServerDownError):
service_definition.send()

# Retrieve the corresponding log
url = reverse('hook-log-list', kwargs={
'parent_lookup_asset': self.hook.asset.uid,
'parent_lookup_hook': self.hook.uid
})

response = self.client.get(url)
first_hooklog_response = response.data.get('results')[0]

# Result should match first try
self.assertEqual(
first_hooklog_response.get('status_code'),
status.HTTP_504_GATEWAY_TIMEOUT,
)
self.assertEqual(
json.loads(first_hooklog_response.get('message')),
first_mock_response,
)
return first_hooklog_response
4 changes: 4 additions & 0 deletions kobo/apps/kobo_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from kobo.apps.openrosa.libs.permissions import get_model_permission_codenames
from kobo.apps.organizations.models import create_organization, Organization
from kpi.utils.database import update_autofield_sequence, use_db
from kpi.utils.permissions import is_user_anonymous


class User(AbstractUser):
Expand Down Expand Up @@ -52,6 +53,9 @@ def is_org_owner(self):
@property
@cache_for_request
def organization(self):
if is_user_anonymous(self):
return

# Database allows multiple organizations per user, but we restrict it to one.
if organization := Organization.objects.filter(
organization_users__user=self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,11 @@ def create(self, request, *args, **kwargs):
username = user.username

if request.method.upper() == 'HEAD':
return Response(status=status.HTTP_204_NO_CONTENT,
headers=self.get_openrosa_headers(request),
template_name=self.template_name)
return Response(
status=status.HTTP_204_NO_CONTENT,
headers=self.get_openrosa_headers(request),
template_name=self.template_name,
)

is_json_request = is_json(request)

Expand Down
Loading
Loading