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
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
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"))
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
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
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} )
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
def test_workflow_not_found(self): with self.assertRaisesRegex(UploadError, "UploadError<404,workflow-not-found>"): with locked_and_loaded_step(999, "abc"): pass