Amazon S3 Utilities

Overview

The Amazon S3 Utilities Plug-in leverages the Amazon AWS Java API to connect with Amazon S3 to store and retrieve files.  

Key Features & Functionality

The following smart services are included:

  • Upload documents to AWS S3
  • Download documents from AWS S3
  • Create Folders in AWS S3
  • Delete documents from AWS S3

The plug-in also includes a function:

  • getPreSignedURLForS3 that generates a V4 pre signed url that expires after 5s. This allows for a short term access grant to a secured resource. It can be used in a WebAPI object to redirect a user from Appian to a resource on S3.

Amazon S3 Utilities supports the following Amazon S3 features:

 

Note:  The plug-in requires Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files when using client side encryption.

(https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html)

 

The Appian Secure Credential Store is leveraged for the credentials to integrate with Amazon S3. Before executing the plug-in, create an new secure credential store with the following 3 attributes.  These values are obtained from Amazon AWS IAM console.

  • accesskeyid: this is the access key id for connecting to AWS S3
  • accesskeysecret: this is the access key secret for connecting to AWS S3
  • kmscmkid: this attribute is only required if using AWS Client Side Encryption
Anonymous
  • When using the function  getPreSignedURLForS3, it is returning a singed URL starting with the domain name as below. 

    https://s3.{bucket-name}.amazonaws.com/

    While my domain name for the S3 bucket in console is as below

    https://{bucket-name}.s3.us-east-1.amazonaws.com/

    Does anybody know, why the function returns the URL an incorrect format. I am passing the region parameter as "us-east-1". Or how can I fix/correct this?

  • v1.3.5 Release Notes
    • Security Updates

  • Not sure if that is possible, you may got some help on S3 forums. Since the web api response is a redirect, the extra headers don't do anything. 

  • Thank Mike ! I am able to download file now...but one more challenge i am facing is that I need to rename the file which is being getting download from presigned URL.I try to add extra header

    Content-Disposition: attachment; filename="test.txt"

    But it is not working

    Any suggestion ?
  • Why are you linking to an integration object? The link needs to be to the web api, otherwise the integration object runs and generates the link once - the web api never reevaluates again.

    You need a link to the web api with a doc id as param, and a redirect and the URL in the Location header.

    Some helpful background reading: en.wikipedia.org/.../HTTP_302

  •       I am facing the same issue ,link is working fine for the first time but after that ,link is giving timeout error....I have generated presigned URL in web api and that web api is being called from interface in safelink .My requirement is to allow user to download document  any number of time...link should be available all the time.Could you please help here. and code sample ?

  • Josh, what is the need for a custom timer? Is it for shortening or extending? Extending the expiration timer beyond a minimum opens up a security hole since the links can be shared and accessed by anyone. As explained in my previous response, the 5s timer is just to issue a redirect to the link once it's signed by AWS, which is more than long enough to do so (it could be argued it's too long still).

  • jeank0002, in case you or anyone else is interested, I used the getawsv4signature from the Cryptographic Hash Functions plugin to create a presigned S3 URL with a custom expiration value (a constant in our case).  Here's the code I used:

    if(a!isNullOrEmpty(ri!fileURI), 
      null, 
      a!localVariables(
        local!resourceURI: if(left(ri!fileURI, 1) = "/", ri!fileURI, "/"&ri!fileURI),
        local!currentTime: now(),
        local!dateTimeStamp: rule!TRSY_formatXAmzDateTime(dateTime: local!currentTime, isDateOnly: false),
        local!dateStamp: rule!TRSY_formatXAmzDateTime(dateTime: local!currentTime, isDateOnly: true),
        local!bucketName: cons!AWS_S3_PROFILE_IMAGE_BUCKET_NAME,
        local!hostname: local!bucketName & "." & cons!AWS_S3_DOMAIN_NAME,
        
        /* CredentialScope = <dateStamp>/<aws-region>/<aws-service>/aws4_request */
        local!credentialScope: joinarray(
          {
            local!dateStamp,
            cons!AWS_S3_REGION,
            cons!AWS_S3_SERVICE_NAME,
            cons!AWS_SIGNATURE_VERSION
          },
          "/"
        ),
        /* Query parameters */
        local!amzParams: {
          a!map(param: "X-Amz-Algorithm", value: cons!AWS_SIGNING_ALGORITHM),
          a!map(param: "X-Amz-Credential", value: cons!AWS_S3_PROFILE_IMAGE_ACCESS_KEY_ID&"/"&local!credentialScope),
          a!map(param: "X-Amz-Date", value: local!dateTimeStamp),
          a!map(param: "X-Amz-Expires", value: cons!AWS_S3_PROFILE_IMAGE_PRESIGNED_URL_EXPIRATION_TIME),
          a!map(param: "X-Amz-SignedHeaders", value: "host")
        },
        local!queryString: joinarray(
          a!forEach(
            items: local!amzParams,
            expression: urlencode(fv!item.param)&"="&urlencode(fv!item.value)
          ),
          "&"
        ),
        
        /* Step 1: Canonical request */
        local!canonicalRequest: a!localVariables(
          local!headers: "host:"&local!hostname,
          joinarray(
            {
              "GET",
              local!resourceURI,
              local!queryString,
              local!headers&char(10),
              "host",
              "UNSIGNED-PAYLOAD"
            },
            char(10)
          )
        ),
        
        /* Step 2: String to sign */
        local!stringToSign: joinarray(
          {
            cons!AWS_SIGNING_ALGORITHM,
            local!dateTimeStamp,
            local!credentialScope,
            sha256hash(local!canonicalRequest)
          },
          char(10)
        ),
        
        /* Step 3: Signature */
        local!signature: getawsv4signature(
          key:"",
          scsValue: {
            cons!AWS_S3_READONLY_PROFILE_IMAGE_SCSFIELD_EXTERNALFIELD,
            cons!AWS_S3_READONLY_PROFILE_IMAGE_SCSFIELD_FIELDNAME_SECRETACCESSKEY
          }, 
          dateStamp: local!dateStamp, 
          regionName: cons!TRSY_SQS_AWS_REGION, 
          serviceName: cons!AWS_S3_SERVICE_NAME, 
          string:local!stringToSign
        ),
        
        /* Return the full presigned URL */
        "https://"&local!hostname&local!resourceURI&"?"&local!queryString&"&X-Amz-Signature="&local!signature
      )
    )

    And here's the code for the TRSY_formatXAmzDateTime rule:

    if( 
      or(
        a!isNullOrEmpty(ri!isDateOnly),
        not(ri!isDateOnly)
      ),
      text(gmt(ri!dateTime,"America/Chicago"), "yyyymmddThhmmss")&"Z",
      text(gmt(ri!dateTime,"America/Chicago"), "yyyymmdd")
    )

  • Hi Mike,

    Thanks for responding so quickly. I am asking Appian Support about the stack trace. The plugin is on version 1.0.0.9 in two of our environments, both of which are on 22.2. It works in one environment, but is throwing the above error in the other.

    I will get more details on stack trace ASAP.


  • Do you have the stack trace? Was the environment upgraded?