Repository: medtagger/MedTagger

Issue:
REST API doesn't validate incoming Label Element's Tags against their existance in Tasks
## Current Behavior

User can send a Label with Label Elements that uses existing Tags but not assigned to Task that he was solving. Such case cannot be sent via UI (only via REST API).

## Steps to Reproduce the Problem

Take an example MedTagger configuration from `.example.medtagger.yml` and send such Label:

**POST**: `/api/v1/scans/{SCAN_ID}/FIND_MIDDLE_OF_THE_SPINE/label`

```json
{
  "elements": [
    {
      "x": 0.1,
      "y": 0.1,
      "slice_index": 10,
      "tag": "NODULE",
      "tool": "POINT"
    }
  ],
  "labeling_time": 1.0,
  "comment": "Dogs > Cats"
}
```

**Reason for the failure:** NODULE is not part of FIND_MIDDLE_OF_THE_SPINE task.


Discussion:

Resolving patch:
diff --git a/backend/medtagger/api/scans/business.py b/backend/medtagger/api/scans/business.py
--- a/backend/medtagger/api/scans/business.py
+++ b/backend/medtagger/api/scans/business.py
@@ -104,19 +104,21 @@ def get_slices_for_scan(scan_id: ScanID, begin: int, count: int,
         yield _slice, image
 
 
-def validate_label_payload(elements: List[Dict], files: Dict[str, bytes]) -> None:
+def validate_label_payload(label: Dict, task_key: str, files: Dict[str, bytes]) -> None:
     """Validate and raise an Exception for sent payload.
 
-    :param elements: List of JSONs describing elements for a single label
+    :param label: JSON describing a single Label
+    :param task_key: key for the Task
     :param files: mapping of uploaded files (name and content)
     """
-    _validate_label_elements(elements, files)
+    _validate_label_elements(label, task_key, files)
     _validate_files(files)
-    _validate_tool(elements)
+    _validate_tool(label)
 
 
-def _validate_tool(elements: List[Dict]) -> None:
+def _validate_tool(label: Dict) -> None:
     """Validate if the tool for given Label Element is available for given tag."""
+    elements = label['elements']
     for label_element in elements:
         tag = _get_label_tag(label_element['tag'])
         if label_element['tool'] not in {tool.name for tool in tag.tools}:
@@ -135,16 +137,20 @@ def _validate_files(files: Dict[str, bytes]) -> None:
             raise InvalidArgumentsException('Type of file "{}" is not supported!'.format(file_name))
 
 
-def _validate_label_elements(elements: List[Dict], files: Dict[str, bytes]) -> None:
+def _validate_label_elements(label: Dict, task_key: str, files: Dict[str, bytes]) -> None:
     """Validate Label Elements and make sure that all Brush Elements have images."""
+    task = TasksRepository.get_task_by_key(task_key)
+    available_tags_keys = {tag.key for tag in task.available_tags}
+    elements = label['elements']
     for label_element in elements:
+        # Check if Label Element Tag is part of this Task
+        if label_element['tag'] not in available_tags_keys:
+            raise InvalidArgumentsException('Tag {} is not part of Task {}.'.format(label_element['tag'], task_key))
+
         # Each Brush Label Element should have its own image attached
-        if label_element['tool'] == LabelTool.BRUSH.value:
-            try:
-                files[label_element['image_key']]
-            except KeyError:
-                message = 'Request does not have field named {} that could contain the image!'
-                raise InvalidArgumentsException(message.format(label_element['image_key']))
+        if label_element['tool'] == LabelTool.BRUSH.value and not files.get(label_element['image_key']):
+            message = 'Request does not have field named {} that could contain the image!'
+            raise InvalidArgumentsException(message.format(label_element['image_key']))
 
 
 def add_label(scan_id: ScanID, task_key: str, elements: List[Dict],   # pylint: disable-msg=too-many-arguments
diff --git a/backend/medtagger/api/scans/service_rest.py b/backend/medtagger/api/scans/service_rest.py
--- a/backend/medtagger/api/scans/service_rest.py
+++ b/backend/medtagger/api/scans/service_rest.py
@@ -132,7 +132,7 @@ def post(scan_id: ScanID, task_key: str) -> Any:
             best_error = best_match(errors)
             raise InvalidArgumentsException(best_error.message)
 
-        business.validate_label_payload(elements, files)
+        business.validate_label_payload(label, task_key, files)
 
         labeling_time = label['labeling_time']
         comment = label.get('comment')


Test patch:
diff --git a/backend/tests/functional_tests/test_adding_new_label.py b/backend/tests/functional_tests/test_adding_new_label.py
--- a/backend/tests/functional_tests/test_adding_new_label.py
+++ b/backend/tests/functional_tests/test_adding_new_label.py
@@ -223,3 +223,46 @@ def test_add_chain_label_not_enough_points(prepare_environment: Any, synchronous
     response = api_client.post('/api/v1/scans/{}/MARK_KIDNEYS/label'.format(scan_id), data=data,
                                headers=get_headers(token=user_token, multipart=True))
     assert response.status_code == 400
+
+
+def test_add_label_with_tag_from_other_task(prepare_environment: Any, synchronous_celery: Any) -> None:
+    """Test for adding a Label with Tag from other Task."""
+    api_client = get_api_client()
+    user_token = get_token_for_logged_in_user('admin')
+
+    # Step 1. Prepare a structure for the test
+    DatasetsRepository.add_new_dataset('KIDNEYS', 'Kidneys')
+    left_task = TasksRepository.add_task('MARK_LEFT', 'Mark Left', 'path/to/image', ['KIDNEYS'], [])
+    right_task = TasksRepository.add_task('MARK_RIGHT', 'Mark Left', 'path/to/image', ['KIDNEYS'], [])
+    LabelTagsRepository.add_new_tag('TAG_LEFT', 'Tag Left', [LabelTool.POINT], left_task.id)
+    LabelTagsRepository.add_new_tag('TAG_RIGHT', 'Tag Right', [LabelTool.POINT], right_task.id)
+
+    # Step 2. Add Scan to the system
+    payload = {'dataset': 'KIDNEYS', 'number_of_slices': 3}
+    response = api_client.post('/api/v1/scans/', data=json.dumps(payload),
+                               headers=get_headers(token=user_token, json=True))
+    assert response.status_code == 201
+    json_response = json.loads(response.data)
+    scan_id = json_response['scan_id']
+
+    # Step 3. Label it with an element with Tag from another Task
+    payload = {
+        'elements': [{
+            'slice_index': 0,
+            'x': 0.25,
+            'y': 0.5,
+            'tag': 'TAG_RIGHT',
+            'tool': LabelTool.POINT.value,
+        }],
+        'labeling_time': 12.34,
+    }
+    data = {
+        'label': json.dumps(payload),
+    }
+    response = api_client.post('/api/v1/scans/{}/MARK_LEFT/label'.format(scan_id), data=data,
+                               headers=get_headers(token=user_token, multipart=True))
+    assert response.status_code == 400
+    json_response = json.loads(response.data)
+    assert isinstance(json_response, dict)
+    assert json_response['message'] == 'Invalid arguments.'
+    assert json_response['details'] == 'Tag TAG_RIGHT is not part of Task MARK_LEFT.'
diff --git a/backend/tests/functional_tests/test_tools_and_tags.py b/backend/tests/functional_tests/test_tools_and_tags.py
--- a/backend/tests/functional_tests/test_tools_and_tags.py
+++ b/backend/tests/functional_tests/test_tools_and_tags.py
@@ -49,9 +49,9 @@ def test_add_label_non_existing_tag(prepare_environment: Any) -> None:
     response = api_client.post('/api/v1/scans/{}/MARK_KIDNEYS/label'.format(scan_id),
                                data={'label': json.dumps(payload)},
                                headers=get_headers(token=user_token, multipart=True))
-    assert response.status_code == 404
+    assert response.status_code == 400
     json_response = json.loads(response.data)
-    assert json_response['details'] == 'Could not find any Label Tag for that key!'
+    assert json_response['details'] == 'Tag NON_EXISTING is not part of Task MARK_KIDNEYS.'
 
 
 def test_add_label_non_supported_tool(prepare_environment: Any) -> None: