Skip to content

Commit

Permalink
feat(accessLogsExport): create new endpoints TASK-1147  (#5304)
Browse files Browse the repository at this point in the history
### 👷 Description for instance maintainers

Create two new endpoints for access log exports which will return the
status of the export job. One endpoint is restricted for superusers and
allows them to access all logs while the other endpoint is for regular
users to access their own logs.

### 👀 Preview steps

## Test `/api/v2/access-logs/export`

1. Login to kpi as a super user.
2. Make a GET request to /api/v2/access-logs/export
3. If this is your first time accessing the endpoint, then verify you
receive a 200 Ok with an empty list:

```json
[]

```

1. If not, verify you receive a 200 Ok with a list of created export
tasks:

```json
[
    {
        "uid": "alezKqwR8diLDQEepA83Rz4V",
        "status": "complete",
        "date_created": "2024-12-03T17:29:21.602245Z"
    },
    {
        "uid": "aleyTYvQjKT4QAcFztyYNHVC",
        "status": "created",
        "date_created": "2024-12-03T00:01:32.580491Z"
    },
    {
        "uid": "alebV6fWMgUARPmCBJDKQGKQ",
        "status": "complete",
        "date_created": "2024-12-02T23:59:47.426752Z"
    }
]
```

1. Make a POST request to /api/v2/access-logs/export and verify you
receive a 202 Accepted with this response:

```json
[
    "status: created"
]

```

1. Logout and try to access the endpoint. Verify you get receive a 401
Unauthorized
with this response:

```json
{
    "detail": "Authentication credentials were not provided."
}

```

1. Login as a regular user and access the endpoint. Verify you receive a
403 Forbidden with this response:

```json
{
    "detail": "You do not have permission to perform this action."
}

```

### Test `/api/v2/access-logs/me/export`

Follow the same testing instructions above but skip number 7 as both
superusers and regular users have the same access to the endpoint.

🟢 Also verify that making a POST request on both endpoints should now
generate an email with the follow format:
```
kpi_worker-1      | MIME-Version: 1.0
kpi_worker-1      | Content-Transfer-Encoding: 7bit
kpi_worker-1      | Subject: Access Log Report Complete
kpi_worker-1      | From: webmaster@localhost
kpi_worker-1      | To: test@test.com
kpi_worker-1      | Date: Wed, 27 Nov 2024 19:38:29 -0000
kpi_worker-1      | Message-ID: <173273630990.23.10232391233968451994@948bfe8772ce>
kpi_worker-1      | 
kpi_worker-1      | Hello test,
kpi_worker-1      | 
kpi_worker-1      | Your report is complete: http://kf.kobo.local:8080/private-media/test/exports/access_logs_export-test-2024-11-27T193829Z.csv
kpi_worker-1      | 
kpi_worker-1      | Regards,
kpi_worker-1      | KoboToolbox
```

---------

Co-authored-by: Rebecca Graber <becca.graber@kobotoolbox.org>
  • Loading branch information
RuthShryock and rgraber authored Dec 4, 2024
1 parent 70407e3 commit 2ae607e
Show file tree
Hide file tree
Showing 4 changed files with 374 additions and 18 deletions.
184 changes: 179 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,180 @@ 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_returns_success(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.assertIn(task.status, ['created', 'processing', 'complete'])
self.assertFalse(task.get_all_logs)

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

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

response_status = self.client.get(self.url)
self.assertEqual(response_status.status_code, status.HTTP_200_OK)

# Assert the response contains a list of tasks
tasks = response_status.json()
self.assertIsInstance(tasks, list)
self.assertGreater(len(tasks), 0) # Ensure at least one task is present

# Assert the structure of the first task in the list
first_task = tasks[0]
self.assertIn('uid', first_task)
self.assertIn('status', first_task)
self.assertIn('date_created', first_task)

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(
'Export task for user access logs already in progress.',
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_returns_success(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.assertIn(task.status, ['created', 'processing', 'complete'])
self.assertTrue(task.get_all_logs)

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

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

response_status = self.client.get(self.url)
self.assertEqual(response_status.status_code, status.HTTP_200_OK)

# Assert the response contains a list of tasks
tasks = response_status.json()
self.assertIsInstance(tasks, list)
self.assertGreater(len(tasks), 0) # Ensure at least one task is present

# Assert the structure of the first task in the list
first_task = tasks[0]
self.assertIn('uid', first_task)
self.assertIn('status', first_task)
self.assertIn('date_created', first_task)

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(
'Export task for all access logs already in progress.',
response_second.json()['error'],
)
14 changes: 13 additions & 1 deletion kobo/apps/audit_log/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
from rest_framework.routers import DefaultRouter

from .views import AccessLogViewSet, AllAccessLogViewSet, AuditLogViewSet
from .views import (
AccessLogsExportViewSet,
AccessLogViewSet,
AllAccessLogsExportViewSet,
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', AllAccessLogsExportViewSet, basename='all-access-logs-export'
)
router.register(
r'access-logs/me/export', AccessLogsExportViewSet, basename='access-logs-export'
)

urlpatterns = []
Loading

0 comments on commit 2ae607e

Please sign in to comment.