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(accessLogsExport): create new endpoints TASK-1147  #5304

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
09373fb
add AccessLogExportTask class, refactored Project View exports to cre…
RuthShryock Nov 5, 2024
4079ea0
revert to before Submissions name change
RuthShryock Nov 14, 2024
eb67d2b
:This reverts commit 4079ea05a8eed915ad74143a43c7be665b70e6a8.
RuthShryock Nov 14, 2024
0f9ac39
Merge branch 'main' of github.com:kobotoolbox/kpi into access-log-exp…
RuthShryock Nov 14, 2024
2836ea3
fix circular import error
RuthShryock Nov 14, 2024
b69a006
fix formatting
RuthShryock Nov 14, 2024
04dc2cd
rename ExportTaskBase, ExportTask, and SynchronousExport to have Subm…
RuthShryock Nov 15, 2024
4c46fb0
fix formatting
RuthShryock Nov 15, 2024
de7f7f5
Merge branch 'main' of github.com:kobotoolbox/kpi into access-log-exp…
RuthShryock Nov 20, 2024
ff45ba8
fix migration error by creating common export tasks for each project …
RuthShryock Nov 20, 2024
7837192
fix formatting
RuthShryock Nov 20, 2024
6c60768
simplify refactor with ExportTaskMixin and adding get_data and defaul…
RuthShryock Nov 26, 2024
43ce879
updating branch with main
RuthShryock Nov 26, 2024
a0a2d5e
add access log export endpoints and tests
RuthShryock Nov 26, 2024
bd5c994
rename functions and variables, fix grammar, and allow for views to b…
RuthShryock Nov 27, 2024
91fefc7
Merge branch 'main' of github.com:kobotoolbox/kpi into endpoints-for-…
RuthShryock Nov 27, 2024
0640937
Merge branch 'access-log-export-task-class' of github.com:kobotoolbox…
RuthShryock Nov 27, 2024
cd25b98
remove view from data for access log exports
RuthShryock Nov 27, 2024
6c3e3af
Revert "refactor(organizations): add of useSession hook TASK-1305 (#…
RuthShryock Nov 27, 2024
dd5650d
Revert "feat(InlineMessage): add new type TASK-987 (#5305)"
RuthShryock Nov 27, 2024
cd35d96
Merge branch 'main' into endpoints-for-access-logs-export
RuthShryock Dec 2, 2024
039063b
Reapply "refactor(organizations): add of useSession hook TASK-1305 (…
RuthShryock Dec 2, 2024
33e1c0a
Reapply "feat(InlineMessage): add new type TASK-987 (#5305)"
RuthShryock Dec 2, 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
152 changes: 147 additions & 5 deletions kobo/apps/audit_log/tests/api/v2/test_api_audit_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
from rest_framework.reverse import reverse

from kobo.apps.audit_log.audit_actions import AuditAction
from kobo.apps.audit_log.models import (
AccessLog,
AuditLog,
AuditType,
)
from kobo.apps.audit_log.models import AccessLog, AuditLog, AuditType
from kobo.apps.audit_log.tests.test_signals import skip_login_access_log
from kobo.apps.kobo_auth.shortcuts import User
from kpi.constants import (
ACCESS_LOG_SUBMISSION_AUTH_TYPE,
ACCESS_LOG_SUBMISSION_GROUP_AUTH_TYPE,
)
from kpi.models.import_export_task import AccessLogExportTask
from kpi.tests.base_test_case import BaseTestCase
from kpi.urls.router_api_v2 import URL_NAMESPACE as ROUTER_URL_NAMESPACE

Expand Down Expand Up @@ -430,3 +427,148 @@ def test_can_search_access_logs_by_date_including_submission_groups(self):
group['metadata']['auth_type'],
ACCESS_LOG_SUBMISSION_GROUP_AUTH_TYPE,
)


class ApiAccessLogsExportTestCase(BaseAuditLogTestCase):

def get_endpoint_basename(self):
return 'access-logs-export-list'

def test_export_as_anonymous_returns_unauthorized(self):
self.client.logout()
response = self.client.post(self.url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

def test_export_for_user_commences(self):
self.force_login_user(User.objects.get(username='anotheruser'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

def test_export_for_superuser_commences(self):
self.force_login_user(User.objects.get(username='admin'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

def test_create_export_task_on_post(self):
test_user = User.objects.get(username='anotheruser')
self.force_login_user(test_user)

response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

task = (
AccessLogExportTask.objects.filter(user=test_user)
.order_by('-date_created')
.first()
)
self.assertIsNotNone(task)
self.assertEqual(task.status, 'complete')

def test_get_status_of_most_recent_task(self):
self.force_login_user(User.objects.get(username='anotheruser'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

response_status = self.client.get(self.url)
self.assertEqual(response_status.status_code, status.HTTP_200_OK)
self.assertIn('uid', response_status.json())
self.assertIn('status', response_status.json())
self.assertEqual(response_status.json()['status'], 'complete')

def test_multiple_export_tasks_not_allowed(self):
test_user = User.objects.get(username='anotheruser')
self.force_login_user(test_user)

response_first = self.client.post(self.url)
self.assertEqual(response_first.status_code, status.HTTP_202_ACCEPTED)

task = (
AccessLogExportTask.objects.filter(user=test_user)
.order_by('-date_created')
.first()
)
task.status = 'processing'
task.save()

response_second = self.client.post(self.url)
self.assertEqual(response_second.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(
'You already have a running export task for your own logs',
response_second.json()['error'],
)


class AllApiAccessLogsExportTestCase(BaseAuditLogTestCase):

def get_endpoint_basename(self):
return 'all-access-logs-export-list'

def test_export_as_anonymous_returns_unauthorized(self):
self.client.logout()
response = self.client.post(self.url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

def test_regular_user_cannot_export_access_logs(self):
self.force_login_user(User.objects.get(username='anotheruser'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_export_access_logs_for_superuser_commences(self):
self.force_login_user(User.objects.get(username='admin'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

def test__superuser_create_export_task_on_post(self):
test_superuser = User.objects.get(username='admin')
self.force_login_user(test_superuser)

response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

task = (
AccessLogExportTask.objects.filter(user=test_superuser)
.order_by('-date_created')
.first()
)
self.assertIsNotNone(task)
self.assertEqual(task.status, 'complete')

def test_superuser_get_status_of_most_recent_task(self):
self.force_login_user(User.objects.get(username='admin'))
response = self.client.post(self.url)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)

response_status = self.client.get(self.url)
self.assertEqual(response_status.status_code, status.HTTP_200_OK)
self.assertIn('uid', response_status.json())
self.assertIn('status', response_status.json())
self.assertEqual(response_status.json()['status'], 'complete')

def test_permission_denied_for_non_superusers_on_get_status(self):
non_superuser = User.objects.get(username='anotheruser')
self.force_login_user(non_superuser)

response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_multiple_export_tasks_not_allowed(self):
test_superuser = User.objects.get(username='admin')
self.force_login_user(test_superuser)

response_first = self.client.post(self.url)
self.assertEqual(response_first.status_code, status.HTTP_202_ACCEPTED)

task = (
AccessLogExportTask.objects.filter(user=test_superuser)
.order_by('-date_created')
.first()
)
task.status = 'processing'
task.save()

response_second = self.client.post(self.url)
self.assertEqual(response_second.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(
'You already have a running export task for this type.',
response_second.json()['error'],
)
13 changes: 12 additions & 1 deletion kobo/apps/audit_log/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from rest_framework.routers import DefaultRouter

from .views import AccessLogViewSet, AllAccessLogViewSet, AuditLogViewSet
from .views import (
AccessLogsExportViewSet,
AccessLogViewSet,
AllAccessLogViewSet,
AuditLogViewSet,
)

router = DefaultRouter()
router.register(r'audit-logs', AuditLogViewSet, basename='audit-log')
router.register(r'access-logs', AllAccessLogViewSet, basename='all-access-logs')
router.register(r'access-logs/me', AccessLogViewSet, basename='access-log')
router.register(
r'access-logs/export', AccessLogsExportViewSet, basename='all-access-logs-export'
)
router.register(
r'access-logs/me/export', AccessLogsExportViewSet, basename='access-logs-export'
)

urlpatterns = []
86 changes: 85 additions & 1 deletion kobo/apps/audit_log/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from rest_framework import mixins, viewsets
from rest_framework import exceptions, mixins, status, viewsets
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.response import Response

from kpi.filters import SearchFilter
from kpi.models.import_export_task import AccessLogExportTask
from kpi.permissions import IsAuthenticated
from kpi.tasks import export_task_in_background
from .filters import AccessLogPermissionsFilter
from .models import AccessLog, AuditLog
from .permissions import SuperUserPermission
Expand Down Expand Up @@ -321,3 +324,84 @@ class AccessLogViewSet(AuditLogViewSet):
permission_classes = (IsAuthenticated,)
filter_backends = (AccessLogPermissionsFilter,)
serializer_class = AccessLogSerializer


class AccessLogsExportViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
lookup_field = 'uid'

def create(self, request, uid=None, type=None, *args, **kwargs):
if not request.user.is_superuser and 'access-logs/export' in request.path:
raise exceptions.PermissionDenied(
'Only superusers can export all access logs.'
)

get_all_logs = 'access-logs/export' in request.path

# Superuser handling: one job for all logs and another for their own logs
if request.user.is_superuser:
# Check if the superuser has a task running for all or just their own logs
if AccessLogExportTask.objects.filter(
user=request.user,
status=AccessLogExportTask.PROCESSING,
get_all_logs=get_all_logs,
).exists():
return Response(
{'error': 'You already have a running export task for this type.'},
status=status.HTTP_400_BAD_REQUEST,
)
else:
# Non-superusers can only run one task for their own logs at a time
if AccessLogExportTask.objects.filter(
user=request.user,
status=AccessLogExportTask.PROCESSING,
get_all_logs=False,
).exists():
return Response(
{
'error': (
'You already have a running export task for your own logs.'
)
},
status=status.HTTP_400_BAD_REQUEST,
)

export_task = AccessLogExportTask.objects.create(
user=request.user,
get_all_logs=get_all_logs,
data={
'type': 'access_logs_export',
},
)

export_task_in_background.delay(
export_task_uid=export_task.uid,
username=export_task.user.username,
export_task_name='kpi.AccessLogExportTask',
)
return Response(
{f'status: {export_task.status}'},
status=status.HTTP_202_ACCEPTED,
)

def list(self, request, *args, **kwargs):
if not request.user.is_superuser and 'access-logs/export' in request.path:
raise exceptions.PermissionDenied(
'Only superusers can export all access logs.'
)

task = (
AccessLogExportTask.objects.filter(user=request.user)
.order_by('-date_created')
.first()
)

if task:
return Response(
{'uid': task.uid, 'status': task.status}, status=status.HTTP_200_OK
)
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

No 404 here. An empty list is not an error response

return Response(
{'error': 'No export task found for this user.'},
status=status.HTTP_404_NOT_FOUND,
)
Loading