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 _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_has_different_api_token(self): step = Step(file_upload_api_token="good-api-token") with self.assertRaisesRegex( UploadError, "UploadError<403,authorization-bearer-token-invalid>"): raise_if_api_token_is_wrong(step, "bad-api-token")
def test_step_has_same_api_token(self): step = Step(file_upload_api_token="good-api-token") raise_if_api_token_is_wrong(step, "good-api-token")
def test_step_has_no_api_token(self): step = Step(file_upload_api_token=None) with self.assertRaisesRegex(UploadError, "UploadError<403,step-has-no-api-token>"): raise_if_api_token_is_wrong(step, "some-api-token")