Beispiel #1
0
def _raise_if_unauthorized(workflow_id: int, step_slug: str,
                           api_token: str) -> None:
    with upload.locked_and_loaded_step(workflow_id, step_slug) as (
            workflow_lock,
            step,
            _,
    ):  # raise UploadError
        upload.raise_if_api_token_is_wrong(step,
                                           api_token)  # raise UploadError
Beispiel #2
0
def _load_step(workflow: Workflow, step_slug: str) -> Step:
    """Return a Step or raise HandlerError."""
    try:
        with upload.locked_and_loaded_step(workflow.id,
                                           step_slug) as (_, step, __):
            pass
    except upload.UploadError as err:
        raise HandlerError("UploadError: %s" % err.error_code)
    return step
Beispiel #3
0
 def test_yielded_values(self):
     _init_module("x", param_id_name="file", param_type="file")
     workflow = Workflow.create_and_init()
     step = workflow.tabs.first().steps.create(
         order=0,
         slug="step-123",
         module_id_name="x",
         file_upload_api_token="abc123",
         params={"file": None},
     )
     with locked_and_loaded_step(workflow.id, "step-123") as x:
         self.assertEqual(x, (workflow, step, "file"))
Beispiel #4
0
 def test_step_module_deleted(self):
     workflow = Workflow.create_and_init()
     workflow.tabs.first().steps.create(
         order=0,
         slug="step-123",
         module_id_name="doesnotexist",
         file_upload_api_token="abc123",
         params={"file": None},
     )
     with self.assertRaisesRegex(UploadError,
                                 "UploadError<400,step-module-deleted>"):
         with locked_and_loaded_step(workflow.id, "step-123"):
             pass
Beispiel #5
0
 def test_step_module_has_no_file_param(self):
     _init_module("x", param_id_name="file", param_type="string")
     workflow = Workflow.create_and_init()
     workflow.tabs.first().steps.create(
         order=0,
         slug="step-123",
         module_id_name="x",
         file_upload_api_token="abc123",
         params={"file": None},
     )
     with self.assertRaisesRegex(UploadError,
                                 "UploadError<400,step-has-no-file-param>"):
         with locked_and_loaded_step(workflow.id, "step-123"):
             pass
Beispiel #6
0
def _finish_upload(data: Dict[str, Any]) -> Dict[str, Any]:
    """Create an UploadedFile by moving data out of tusd's bucket.

    Return kwargs for SetStepParams.
    """
    # SECURITY: we expect metadata to come from Workbench itself. (On
    # production, there's no route from the Internet to tusd's POST endpoint.)
    # However, let's cast to correct types just to be safe. If a miscreant
    # comes along, that'll cause a 500 error and we'll be notified. (Better
    # than sending untrusted data to Django ORM.)
    # Raise TypeError, KeyError, ValueError.
    filename = str(data["MetaData"]["filename"])
    api_token = str(data["MetaData"]["apiToken"])
    workflow_id = int(data["MetaData"]["workflowId"])
    step_slug = data["MetaData"]["stepSlug"]
    size = int(data["Size"])
    bucket = str(data["Storage"]["Bucket"])
    key = str(data["Storage"]["Key"])

    if bucket != s3.TusUploadBucket:
        # security: if a hijacker manages to craft a request here, prevent its
        # creator from copying a file he/she can't see. (The creator is only
        # known to be able to see `key` of `s3.TusUploadBucket`.)
        raise RuntimeError("SECURITY: did tusd send this request?")

    suffix = PurePath(filename).suffix
    file_uuid = str(uuid.uuid4())
    final_key = None

    with upload.locked_and_loaded_step(workflow_id, step_slug) as (
        workflow,
        step,
        param_id_name,
    ):  # raise UploadError
        # Ensure upload's API token is the same as the one we sent tusd.
        #
        # This doesn't give security: an attacker can simulate a request from
        # tusd with api_token=None and it will look like a browser-initiated
        # one.
        #
        # It's for timing: if the user resets a module's API token, we should
        # disallow all prior uploads.
        if api_token:  # empty when React client uploads
            upload.raise_if_api_token_is_wrong(step, api_token)  # raise UploadError

        final_key = step.uploaded_file_prefix + str(file_uuid) + suffix

        # Tricky leak here: if there's an exception or crash, the transaction
        # is reverted. final_key will remain in S3 but the database won't point
        # to it.
        #
        # Not a huge deal, because `final_key` is in the Step's own directory.
        # The user can delete all leaked files by deleting the Step.
        s3.copy(
            s3.UserFilesBucket,
            final_key,
            f"{bucket}/{key}",
            MetadataDirective="REPLACE",
            ContentDisposition=s3.encode_content_disposition(filename),
            ContentType="application/octet-stream",
        )

        step.uploaded_files.create(
            name=filename, size=size, uuid=file_uuid, key=final_key
        )
        delete_old_files_to_enforce_storage_limits(step=step)
        s3.remove(bucket, key)

    return dict(
        workflow_id=workflow_id, step=step, new_values={param_id_name: file_uuid}
    )
Beispiel #7
0
 def test_step_not_found(self):
     workflow = Workflow.create_and_init()
     with self.assertRaisesRegex(UploadError,
                                 "UploadError<404,step-not-found>"):
         with locked_and_loaded_step(workflow.id, "abc"):
             pass
Beispiel #8
0
 def test_workflow_not_found(self):
     with self.assertRaisesRegex(UploadError,
                                 "UploadError<404,workflow-not-found>"):
         with locked_and_loaded_step(999, "abc"):
             pass