import datetime from django.contrib.auth.models import User from guardian.shortcuts import assign_perm, get_objects_for_user from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from rest_framework_jwt.settings import api_settings from app import pending_actions from app.models import Project, Task from app.plugins.signals import processing_node_removed from app.tests.utils import catch_signal from nodeodm import status_codes from nodeodm.models import ProcessingNode from .classes import BootTestCase from webodm import settings class TestApi(BootTestCase): def setUp(self): pass def tearDown(self): pass def test_projects_and_tasks(self): client = APIClient() user = User.objects.get(username="testuser") self.assertFalse(user.is_superuser) other_user = User.objects.get(username="testuser2") project = Project.objects.create( owner=user, name="test project" ) other_project = Project.objects.create( owner=other_user, name="another test project" ) # Forbidden without credentials res = client.get('/api/projects/') self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) client.login(username="testuser", password="test1234") res = client.get('/api/projects/') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) > 0) res = client.get('/api/projects/?page=1') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data['results']) > 0) # Can sort res = client.get('/api/projects/?ordering=-created_at&page=1') last_project = Project.objects.filter(owner=user).latest('created_at') self.assertTrue(res.data["results"][0]['id'] == last_project.id) res = client.get('/api/projects/{}/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) res = client.get('/api/projects/dasjkldas/') self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) res = client.get('/api/projects/{}/'.format(other_project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can filter res = client.get('/api/projects/?name=999&page=1') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data["results"]) == 0) # Cannot list somebody else's project without permission res = client.get('/api/projects/?id={}&page=1'.format(other_project.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data["results"]) == 0) # Can access individual project res = client.get('/api/projects/{}/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data["id"] == project.id) # Cannot access project for which we have no access to res = client.get('/api/projects/{}/'.format(other_project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can create project, but owner cannot be set res = client.post('/api/projects/', {'name': 'test', 'description': 'test descr'}) self.assertEqual(res.status_code, status.HTTP_201_CREATED) self.assertTrue(Project.objects.get(pk=res.data['id']).owner.id == user.id) # Cannot leave name empty res = client.post('/api/projects/', {'description': 'test descr'}) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) # Create some tasks task = Task.objects.create(project=project) task2 = Task.objects.create(project=project, created_at=task.created_at + datetime.timedelta(0, 1)) other_task = Task.objects.create(project=other_project) # Can list project tasks to a project we have access to res = client.get('/api/projects/{}/tasks/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 2) # Can sort res = client.get('/api/projects/{}/tasks/?ordering=created_at'.format(project.id)) self.assertTrue(res.data[0]['id'] == str(task.id)) self.assertTrue(res.data[1]['id'] == str(task2.id)) res = client.get('/api/projects/{}/tasks/?ordering=-created_at'.format(project.id)) self.assertTrue(res.data[0]['id'] == str(task2.id)) self.assertTrue(res.data[1]['id'] == str(task.id)) # Cannot list project tasks for a project we don't have access to res = client.get('/api/projects/{}/tasks/'.format(other_project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Cannot list project tasks for a project that doesn't exist res = client.get('/api/projects/999/tasks/') self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can list task details for a task belonging to a project we have access to res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, task.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data["id"] == str(task.id)) # images_count field exists self.assertTrue(res.data["images_count"] == 0) # can_rerun_from field exists, should be an empty list at this point self.assertTrue(len(res.data["can_rerun_from"]) == 0) # Get console output res = client.get('/api/projects/{}/tasks/{}/output/'.format(project.id, task.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data == "") task.console.reset("line1\nline2\nline3") task.save() res = client.get('/api/projects/{}/tasks/{}/output/'.format(project.id, task.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data == task.console.output()) # Console output with line num res = client.get('/api/projects/{}/tasks/{}/output/?line=2'.format(project.id, task.id)) self.assertTrue(res.data == "line3") # Console output with line num out of bounds res = client.get('/api/projects/{}/tasks/{}/output/?line=3'.format(project.id, task.id)) self.assertTrue(res.data == "") res = client.get('/api/projects/{}/tasks/{}/output/?line=-1'.format(project.id, task.id)) self.assertTrue(res.data == task.console.output()) # Cannot list task details for a task belonging to a project we don't have access to res = client.get('/api/projects/{}/tasks/{}/'.format(other_project.id, other_task.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # As above, but by trying to trick the API by using a project we have access to res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, other_task.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Cannot duplicate a project we have no access to res = client.post('/api/projects/{}/duplicate/'.format(other_project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can duplicate a project we have access to res = client.post('/api/projects/{}/duplicate/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data.get('success')) new_project_id = res.data['project']['id'] self.assertNotEqual(new_project_id, project.id) # Tasks have been duplicated duplicated_project = Project.objects.get(pk=new_project_id) self.assertEqual(project.task_set.count(), duplicated_project.task_set.count()) # Cannot access task details for a task that doesn't exist res = client.get('/api/projects/{}/tasks/4004d1e9-ed2c-4983-8b93-fc7577ee6d89/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Cannot access task details for a malformed task id res = client.get('/api/projects/{}/tasks/0/'.format(project.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can update a task res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'name': 'updated!'}, format='json') self.assertEqual(res.status_code, status.HTTP_200_OK) # Verify the task has been updated res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, task.id)) self.assertTrue(res.data["name"] == "updated!") # Cannot update a task we have no access to res = client.patch('/api/projects/{}/tasks/{}/'.format(other_project.id, other_task.id), {'name': 'updated!'}, format='json') self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can cancel a task for which we have permission self.assertTrue(task.pending_action is None) res = client.post('/api/projects/{}/tasks/{}/cancel/'.format(project.id, task.id)) self.assertTrue(res.data["success"]) task.refresh_from_db() # Task should have been canceled self.assertTrue(task.last_error is None) self.assertEqual(task.status, status_codes.CANCELED) res = client.post('/api/projects/{}/tasks/{}/restart/'.format(project.id, task.id)) self.assertTrue(res.data["success"]) task.refresh_from_db() # Task should have failed to be restarted self.assertTrue("has no processing node" in task.last_error) # Cannot cancel, restart or delete a task for which we don't have permission for action in ['cancel', 'remove', 'restart']: res = client.post('/api/projects/{}/tasks/{}/{}/'.format(other_project.id, other_task.id, action)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) # Can delete res = client.post('/api/projects/{}/tasks/{}/remove/'.format(project.id, task.id)) self.assertTrue(res.data["success"]) self.assertFalse(Task.objects.filter(id=task.id).exists()) task = Task.objects.create(project=project) temp_project = Project.objects.create(owner=user) # We have permissions to do anything on a project that we own res = client.get('/api/projects/{}/'.format(project.id)) for perm in ['delete', 'change', 'view', 'add']: self.assertTrue(perm in res.data['permissions']) # Can delete project that we we own res = client.delete('/api/projects/{}/'.format(temp_project.id)) self.assertTrue(res.status_code == status.HTTP_204_NO_CONTENT) self.assertTrue(Project.objects.filter(id=temp_project.id).count() == 0) # Really deleted # Cannot delete a project we don't own other_temp_project = Project.objects.create(owner=other_user) res = client.delete('/api/projects/{}/'.format(other_temp_project.id)) self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND) assign_perm('view_project', user, other_temp_project) # We have view permissions only res = client.get('/api/projects/{}/'.format(other_temp_project.id)) self.assertTrue('view' in res.data['permissions']) for perm in ['delete', 'change', 'add']: self.assertFalse(perm in res.data['permissions']) # Can delete a project for which we just have view permissions # (we will just remove our read permissions without deleting the project) res = client.delete('/api/projects/{}/'.format(other_temp_project.id)) self.assertTrue(res.status_code == status.HTTP_204_NO_CONTENT) # Project still exists self.assertTrue(Project.objects.filter(id=other_temp_project.id).count() == 1) # We just can't access it res = client.get('/api/projects/{}/'.format(other_temp_project.id)) self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND) # A user cannot reassign a task to a # project for which he/she has no permissions res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'project': other_project.id}, format='json') self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) # A user cannot reassign a task to a # project for which he/she has no permissions (using uppercase) res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'PROJECT': other_project.id}, format='json') # Request went through, but no changes were applied self.assertEqual(res.status_code, status.HTTP_200_OK) task.refresh_from_db() self.assertTrue(task.project.id == project.id) # A user cannot update a task's read only fields self.assertTrue(task.pending_action != 0) res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), { 'processing_time': 1234, 'status': -99, 'last_error': 'yo!', 'created_at': 0, 'pending_action': 0, 'can_rerun_from': ['abc'] }, format='json') # Operation should fail without errors, but nothing has changed in the DB self.assertEqual(res.status_code, status.HTTP_200_OK) task.refresh_from_db() self.assertTrue(task.processing_time != 1234) self.assertTrue(task.status != -99) self.assertTrue(task.last_error != 'yo!') self.assertTrue(task.created_at != 0) self.assertTrue(task.pending_action != 0) self.assertTrue(len(res.data['can_rerun_from']) == 0) def test_processingnodes(self): client = APIClient() pnode = ProcessingNode.objects.create( hostname="localhost", port=999 ) another_pnode = ProcessingNode.objects.create( hostname="localhost", port=998 ) # Cannot list processing nodes as guest res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) res = client.get('/api/processingnodes/{}/'.format(pnode.id)) self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) # Cannot get options as guest res = client.get('/api/processingnodes/options/') self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) client.login(username="testuser", password="test1234") # Cannot list processing nodes, unless permissions have been granted res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 0) user = User.objects.get(username="testuser") self.assertFalse(user.is_staff) self.assertFalse(user.is_superuser) self.assertFalse(user.has_perm('view_processingnode', pnode)) assign_perm('view_processingnode', user, pnode) self.assertTrue(user.has_perm('view_processingnode', pnode)) # Now we can list processing nodes as normal user res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 1) self.assertTrue(res.data[0]["hostname"] == "localhost") # Can use filters res = client.get('/api/processingnodes/?id={}'.format(pnode.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 1) res = client.get('/api/processingnodes/?id={}'.format(another_pnode.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 0) # Can filter nodes with valid options res = client.get('/api/processingnodes/?has_available_options=true') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 0) res = client.get('/api/processingnodes/?has_available_options=false') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 1) self.assertTrue(res.data[0]['hostname'] == 'localhost') # Can get single processing node as normal user res = client.get('/api/processingnodes/{}/'.format(pnode.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(res.data["hostname"] == "localhost") # Verify online field exists self.assertTrue("online" in res.data) # Should be set to false self.assertFalse(res.data['online']) # Verify max images field self.assertTrue("max_images" in res.data) # Verify engine version self.assertTrue("engine_version" in res.data) # Verify engine self.assertTrue("engine" in res.data) # label should be hostname:port (since no label is set) self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port)) # If we update the label, the label is used instead pnode.label = "Test" pnode.save() res = client.get('/api/processingnodes/{}/'.format(pnode.id)) self.assertEqual(res.data['label'], "Test") # Cannot delete a processing node as normal user res = client.delete('/api/processingnodes/{}/'.format(pnode.id)) self.assertTrue(res.status_code, status.HTTP_403_FORBIDDEN) # Cannot create a processing node as normal user res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'}) self.assertTrue(res.status_code, status.HTTP_403_FORBIDDEN) client.login(username="testsuperuser", password="test1234") # Can delete a processing node as super user # and a signal is sent when a processing node is deleted with catch_signal(processing_node_removed) as h1: res = client.delete('/api/processingnodes/{}/'.format(pnode.id)) self.assertTrue(res.status_code, status.HTTP_200_OK) h1.assert_called_once_with(sender=ProcessingNode, processing_node_id=pnode.id, signal=processing_node_removed) # Can create a processing node as super user res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'}) self.assertTrue(res.status_code, status.HTTP_200_OK) # Verify node has been created res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 2) self.assertTrue(res.data[1]["port"] == 1000) # Test available_options intersection # (with normal user) client.login(username="testuser", password="test1234") user = User.objects.get(username="testuser") self.assertFalse(user.is_superuser) p1 = ProcessingNode.objects.create(hostname="invalid-host", port=11223, last_refreshed=timezone.now(), available_options=[{'name': 'a'}, {'name': 'b'}]) p2 = ProcessingNode.objects.create(hostname="invalid-host-2", port=11223, last_refreshed=timezone.now(), available_options=[{'name': 'a'}, {'name': 'c'}]) p3 = ProcessingNode.objects.create(hostname="invalid-host-3", port=11223, last_refreshed=timezone.now(), available_options=[{'name': 'd'}]) p4 = ProcessingNode.objects.create(hostname="invalid-host-4", port=11223, last_refreshed=timezone.now() - datetime.timedelta(minutes=settings.NODE_OFFLINE_MINUTES * 2), available_options=[{'name': 'd'}]) # offline assign_perm('view_processingnode', user, p1) assign_perm('view_processingnode', user, p2) assign_perm('view_processingnode', user, p4) self.assertFalse(user.has_perm('view_processingnode', p3)) nodes_available = get_objects_for_user(user, 'view_processingnode', ProcessingNode, accept_global_perms=False).exclude(available_options=dict()) self.assertTrue(len(nodes_available) == 3) res = client.get('/api/processingnodes/options/') self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTrue(len(res.data) == 1) self.assertTrue(res.data[0]['name'] == 'a') def test_token_auth(self): client = APIClient() pnode = ProcessingNode.objects.create( hostname="localhost", port=999 ) # Cannot access resources res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) # Cannot generate token with invalid credentials res = client.post('/api/token-auth/', { 'username': 'testuser', 'password': 'wrongpwd' }) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) # Can generate token with valid credentials res = client.post('/api/token-auth/', { 'username': 'testuser', 'password': 'test1234' }) self.assertEqual(res.status_code, status.HTTP_200_OK) token = res.data['token'] self.assertTrue(len(token) > 0) # Can access resources by passing token via querystring res = client.get('/api/processingnodes/?jwt={}'.format(token)) self.assertEqual(res.status_code, status.HTTP_200_OK) # Can access resources by passing token via header client = APIClient(HTTP_AUTHORIZATION="{0} {1}".format(api_settings.JWT_AUTH_HEADER_PREFIX, token)) res = client.get('/api/processingnodes/') self.assertEqual(res.status_code, status.HTTP_200_OK)