Transfer After Approval
Description
Allow users to submit an approval request to transfer a file to a destination Guest Collection.
The request will come in the form of emails to curators who are authorized to approve the transfer. If approved, the file will be transferred using the flow’s own identity — not the user’s — ensuring that users do not need permission to read or write to the destination collection.
Users who run the flow will not be informed who the curators/approvers are, nor will they be informed by the flow itself where the file is transferred to. They only need to know that "this flow allows you to submit a file for transfer."
Example uses include:
-
Allowing researchers to submit research data and findings for consideration and publication.
-
Accepting vulnerability analyses from unknown sources.
Highlights
This flow has several technical highlights.
-
__Private_Parameters
and_private
parameter prefixes are used throughout. This helps restrict visibility of private information (such as email addresses and SMTP credentials) when users view the flow definition as well as while the flow runs.For more information, see Protecting Secrets in the Authoring Flows documentation.
-
By setting the
RunAs
value in theTransferFile
state, the transfer operation will be performed using the flow’s own identity to access the destination collection. This allows the flow to be made public — and run by any user — without having to give individual users write permission on the destination collection.
Prerequisites
The flow definition must be modified before using it to create a new flow. It also has several requirements when running.
Modification requirements
-
The
SetupEmailLoop
state must be modified. The email addresses of approvers must be updated, and unique IDs must be assigned to each reviewer. Finally, theloop_end
value must be updated to match the number of approvers. -
The
SendEmail
state must be modified. Specifically, the SMTP hostname, username, and password insend_credentials
must be updated, as well as the email address insender
.It is also possible to use an AWS SES credential for sending emails. See the Notification action provider documentation for more details.
-
The
TransferFile
state must be modified. Thedestination_endpoint
must be set to the destination Guest Collection ID that will receive approved files, and thedestination_path
should match the target directory you want approved files transferred to.
Execution requirements
-
After modifying the flow definition and creating the flow in the Globus Flows service, a destination collection administrator must give the flow client write permission on the collection.
This can be accomplished in the Web App, in the destination collection’s Permissions tab, by clicking the "Add Permissions" button and searching for the flow ID as the username to share with, and ensuring that the "Write" checkbox is checked.
It’s also possible to use the Globus CLI to accomplish this, using the
globus endpoint role create
command:globus endpoint permission create $DESTINATION_COLLECTION_ID:/ --permissions rw --identity $FLOW_ID
-
Users who seek approval to transfer a file must select a file on a Guest Collection on which they can grant new permissions.
These execution requirements allow the flow — acting as the user — to give itself read permission on the user’s guest collection, and then — acting as the flow itself — to transfer the user’s file to the destination collection.
Source code
{
"Comment": "Request approval to transfer a file from the requester's Guest Collection into a destination collection. If approved, a transfer will be performed using the flow's identity (meaning the flow itself will need write access on the destination collection).",
"StartAt": "GetSourceCollectionInfo",
"States": {
"GetSourceCollectionInfo": {
"Type": "Action",
"Next": "ValidateSourceCollectionType",
"ActionUrl": "https://transfer.actions.globus.org/collection_info",
"ResultPath": "$.collection_info",
"Parameters": {
"endpoint_id.$": "$.source.id"
}
},
"ValidateSourceCollectionType": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.collection_info.details.entity_type",
"StringEquals": "GCSv5_guest_collection",
"Next": "GetSourcePathInfo"
}
],
"Default": "FailSourceCollectionType"
},
"GetSourcePathInfo": {
"Type": "Action",
"Next": "ValidateSourcePathType",
"ActionUrl": "https://transfer.actions.globus.org/stat",
"Parameters": {
"endpoint_id.$": "$.source.id",
"path.$": "$.source.path"
},
"ResultPath": "$.source_stat"
},
"ValidateSourcePathType": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.source_stat.details.type",
"StringEquals": "file",
"Next": "SetupEmailLoop"
}
],
"Default": "FailSourcePathType"
},
"SetupEmailLoop": {
"Comment": "Each random ID for each `reviewer` should be unique and unguessable, and `loop_end` should match the number of `reviewers` defined here.",
"Type": "Pass",
"Next": "SendEmail",
"Parameters": {
"__Private_Parameters": [
"reviewers",
"loop_index",
"loop_end"
],
"reviewers": [
{
"email": "reviewer1@domain.example",
"random_id": "e8c63401"
},
{
"email": "reviewer2@domain.example",
"random_id": "7fc71f01"
}
],
"loop_index": 0,
"loop_end": 2
},
"ResultPath": "$._private_state"
},
"SendEmail": {
"Type": "Action",
"Next": "IncrementLoopIndex",
"ActionUrl": "https://actions.globus.org/notification/notify",
"ResultPath": "$._private_email_result",
"Parameters": {
"__Private_Parameters": [
"body_mimetype",
"body_template",
"body_variables",
"destination",
"send_credentials",
"sender",
"subject"
],
"body_mimetype": "text/html",
"body_template": "<p>The following file has been submitted for review. It must be approved before it can be transferred.</p><p><table><tr><td>Submitter</td><td><code>${SUBMITTER}</code></td></tr><tr><td>Filename</td><td><code>${FILE_NAME}</code></td></tr><tr><td>Size (bytes)</td><td><code>${FILE_SIZE}</code></td></tr></table></p><p><ul><li><a href=\"${APPROVAL_URL}\">✅ Approve transfer</a></li><li><a href=\"${REJECTION_URL}\">❌ Reject transfer</a></li></ul></p>",
"body_variables": {
"FILE_NAME.$": "$.source_stat.details.name",
"FILE_SIZE.$": "$.source_stat.details.size",
"SUBMITTER.=": "(_context.username + ' <' + _context.email + '>') if _context.email and (_context.username != _context.email) else _context.username",
"APPROVAL_URL.=": "'https://actions.globus.org/weboption/option/' + _context.run_id + '-' + _private_state.reviewers[_private_state.loop_index].random_id + '-approve'",
"REJECTION_URL.=": "'https://actions.globus.org/weboption/option/' + _context.run_id + '-' + _private_state.reviewers[_private_state.loop_index].random_id + '-reject'"
},
"destination.=": "_private_state.reviewers[_private_state.loop_index].email",
"send_credentials": [
{
"credential_type": "smtp",
"credential_value": {
"hostname": "smtp.domain.example",
"username": "email@domain.example",
"password": "email-password",
"port": 587
}
}
],
"sender": "flows@domain.example",
"subject": "Approval needed for file transfer"
}
},
"IncrementLoopIndex": {
"Type": "ExpressionEval",
"Next": "ExitEmailLoop",
"Parameters": {
"__Private_Parameters": [
"reviewers",
"loop_index",
"loop_end"
],
"reviewers.$": "$._private_state.reviewers",
"loop_index.=": "_private_state.loop_index + 1",
"loop_end.$": "$._private_state.loop_end"
},
"ResultPath": "$._private_state"
},
"ExitEmailLoop": {
"Type": "Choice",
"Choices": [
{
"Variable": "$._private_state.loop_index",
"NumericLessThanPath": "$._private_state.loop_end",
"Next": "SendEmail"
}
],
"Default": "AddOptions"
},
"AddOptions": {
"Type": "ExpressionEval",
"Parameters": {
"__Private_Parameters": [
"options"
],
"options.=": "[{'name': word, 'url_suffix': _context.run_id + '-' + reviewer.random_id + '-' + word} for reviewer in _private_state.reviewers for word in ['approve', 'reject']]"
},
"ResultPath": "$._private_state",
"Next": "WaitForApproval"
},
"WaitForApproval": {
"Type": "Action",
"Next": "DetermineOutcome",
"ActionUrl": "https://actions.globus.org/weboption/wait_for_option",
"Parameters": {
"__Private_Parameters": [
"options"
],
"options.$": "$._private_state.options"
},
"ResultPath": "$._private_judgement"
},
"DetermineOutcome": {
"Type": "Choice",
"Choices": [
{
"Variable": "$._private_judgement.details.name",
"StringEquals": "approve",
"Next": "GetExistingPathPermissions"
}
],
"Default": "SubmissionRejected"
},
"GetExistingPathPermissions": {
"Type": "Action",
"Next": "CalculateAvailablePermissions",
"ActionUrl": "https://transfer.actions.globus.org/manage_permission",
"ResultPath": "$.permission_list",
"Parameters": {
"operation": "LIST",
"endpoint_id.$": "$.source.id"
}
},
"CalculateAvailablePermissions": {
"Type": "ExpressionEval",
"Next": "DetermineWhetherToCreatePermission",
"ResultPath": "$.permission_judgement",
"Parameters": {
"access_id.=": "([permission['id'] for permission in permission_list.details.DATA if permission['principal'] == _context.flow_id and permission['principal_type'] == 'identity' and permission['path'] == source.path.rsplit('/', 1)[0] + '/'] or [''])[0]"
}
},
"DetermineWhetherToCreatePermission": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.permission_judgement.access_id",
"StringEquals": "",
"Next": "CreatePermission"
}
],
"Default": "TransferFile"
},
"CreatePermission": {
"Type": "Action",
"Next": "TransferFile",
"ActionUrl": "https://transfer.actions.globus.org/manage_permission",
"ResultPath": "$.permission_creation",
"Parameters": {
"operation": "CREATE",
"endpoint_id.$": "$.source.id",
"path.=": "source.path.rsplit('/')[0] + '/'",
"principal.$": "$._context.flow_id",
"principal_type": "identity",
"permissions": "r"
}
},
"TransferFile": {
"Comment": "The destination_path injects the requesting user's ID into the filename to help prevent other files from getting erased.",
"Type": "Action",
"RunAs": "Flow",
"ResultPath": "$._private_transfer_result",
"Next": "DeletePermission",
"ActionUrl": "https://transfer.actions.globus.org/transfer",
"Parameters": {
"__Private_Parameters": [
"source_endpoint",
"destination_endpoint",
"DATA"
],
"source_endpoint.$": "$.source.id",
"destination_endpoint": "00000000-bacb-424d-bbbe-be786aacd771",
"DATA": [
{
"source_path.$": "$.source.path",
"destination_path.=": "'/Inbox/' + _context.user_id.split(':')[-1] + '-' + source_stat.details.name"
}
]
}
},
"DeletePermission": {
"Type": "Action",
"Next": "SubmissionAccepted",
"ActionUrl": "https://transfer.actions.globus.org/manage_permission",
"ResultPath": "$.permission_deletion",
"Parameters": {
"operation": "DELETE",
"endpoint_id.$": "$.source.id",
"rule_id.=": "permission_judgement.access_id or permission_creation.details.access_id"
}
},
"FailSourceCollectionType": {
"Type": "Fail",
"Error": "IncorrectSourceCollectionType",
"Cause": "The source collection must be a Guest Collection"
},
"FailSourcePathType": {
"Type": "Fail",
"Error": "IncorrectSourcePathType",
"Cause": "Only files may be selected when using this flow."
},
"SubmissionRejected": {
"Type": "Pass",
"End": true
},
"SubmissionAccepted": {
"Type": "Pass",
"End": true
}
}
}
{
"required": [
"source"
],
"additionalProperties": false,
"properties": {
"source": {
"title": "File to submit for approval",
"description": "The file selected here will be reviewed prior to transfer. Note that the source collection must be a Guest Collection that you have the ability to add permissions to.",
"type": "object",
"format": "globus-collection",
"required": [
"id",
"path"
],
"properties": {
"id": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
}
}
{
"source": {
"id": "00000000-f206-43d2-b5bb-2c9dc579337d",
"path": "/publication-submission.pdf"
}
}