Repository: aws/aws-sdk-js
Pull request #2987 (2019-12-03 19:00:42)

Support accessing bucket via access point ARN
* add ARN parser and validator to util

* implement s3UseArnRegion config

* implement validating access point ARN

* implement ARN builder utility

* implement populating uri from access point ARN

* re-organize setupRequestListeners()

* throw when client region is fips region because it is not supported

<!--
Thank you for your pull request. Please provide a description below.
-->

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] `npm run test` passes
- [x] changelog is added, `npm run add-change`
- [x] run `bundle exec rake docs:api` and inspect `doc/latest/index.html` if documentation is changed


codecov-io: # [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=h1) Report
> Merging [#2987](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=desc) into [master](https://codecov.io/gh/aws/aws-sdk-js/commit/2b5e3170811a3996dda33b1b7d7d29e20679b621?src=pr&el=desc) will **decrease** coverage by `0.33%`.
> The diff coverage is `73.28%`.

[![Impacted file tree graph](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/graphs/tree.svg?width=650&token=qXdPGBZWAr&height=150&src=pr)](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2987      +/-   ##
==========================================
- Coverage   96.98%   96.64%   -0.34%     
==========================================
  Files         317      317              
  Lines        9245     9363     +118     
  Branches     1697     1742      +45     
==========================================
+ Hits         8966     9049      +83     
- Misses        279      314      +35
```


| [Impacted Files](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/config.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/diff?src=pr&el=tree#diff-bGliL2NvbmZpZy5qcw==) | `87.64% <ø> (ø)` | :arrow_up: |
| [lib/util.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/diff?src=pr&el=tree#diff-bGliL3V0aWwuanM=) | `94.87% <100%> (+0.14%)` | :arrow_up: |
| [lib/service.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/diff?src=pr&el=tree#diff-bGliL3NlcnZpY2UuanM=) | `98.14% <100%> (ø)` | :arrow_up: |
| [lib/region\_config.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/diff?src=pr&el=tree#diff-bGliL3JlZ2lvbl9jb25maWcuanM=) | `82.22% <20%> (-17.78%)` | :arrow_down: |
| [lib/services/s3.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2987/diff?src=pr&el=tree#diff-bGliL3NlcnZpY2VzL3MzLmpz) | `93.46% <74.76%> (-4.56%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=footer). Last update [2b5e317...8d763ee](https://codecov.io/gh/aws/aws-sdk-js/pull/2987?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.changes/next-release/feature-Bucket-a1a45ab9.json b/.changes/next-release/feature-Bucket-a1a45ab9.json
new file mode 100644
index 000000000..d4e639393
--- /dev/null
+++ b/.changes/next-release/feature-Bucket-a1a45ab9.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "Bucket",
+  "description": "Add support for S3 access point. Access Points provide a customizable way to access the objects in a bucket, with a unique hostname and access policy that enforces the specific permissions and network controls for any request made through the access point."
+}
\ No newline at end of file
diff --git a/lib/config.js b/lib/config.js
index 3939d2a96..7da15580c 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -87,6 +87,10 @@ var PromisesDependency;
  *     request to global endpoints or 'us-east-1' regional endpoints. This config is only
  *     applicable to S3 client;
  *     Defaults to 'legacy'
+ * @!attribute s3UseArnRegion
+ *   @return [Boolean] whether to override the request region with the region inferred
+ *     from requested resource's ARN. Only available for S3 buckets
+ *     Defaults to `true`
  *
  * @!attribute useAccelerateEndpoint
  *   @note This configuration option is only compatible with S3 while accessing
@@ -248,6 +252,9 @@ AWS.Config = AWS.util.inherit({
    *   is set to 'us-east-1', whether to send s3 request to global endpoints or
    *   'us-east-1' regional endpoints. This config is only applicable to S3 client.
    *   Defaults to `legacy`
+   * @option options s3UseArnRegion [Boolean] whether to override the request region
+   *   with the region inferred from requested resource's ARN. Only available for S3 buckets
+   *   Defaults to `true`
    *
    * @option options retryDelayOptions [map] A set of options to configure
    *   the retry delay on retryable errors. Currently supported options are:
@@ -534,6 +541,7 @@ AWS.Config = AWS.util.inherit({
     s3BucketEndpoint: false,
     s3DisableBodySigning: true,
     s3UsEast1RegionalEndpoint: 'legacy',
+    s3UseArnRegion: undefined,
     computeChecksums: true,
     convertResponseTypes: true,
     correctClockSkew: false,
diff --git a/lib/region_config.js b/lib/region_config.js
index 1520aaa5f..79052c1a9 100644
--- a/lib/region_config.js
+++ b/lib/region_config.js
@@ -66,7 +66,28 @@ function configureEndpoint(service) {
   }
 }
 
+function getEndpointSuffix(region) {
+  var regionRegexes = {
+    '^(us|eu|ap|sa|ca|me)\\-\\w+\\-\\d+$': 'amazonaws.com',
+    '^cn\\-\\w+\\-\\d+$': 'amazonaws.com.cn',
+    '^us\\-gov\\-\\w+\\-\\d+$': 'amazonaws.com',
+    '^us\\-iso\\-\\w+\\-\\d+$': 'c2s.ic.gov',
+    '^us\\-isob\\-\\w+\\-\\d+$': 'sc2s.sgov.gov'
+  };
+  var defaultSuffix = 'amazonaws.com';
+  var regexes = Object.keys(regionRegexes);
+  for (var i = 0; i < regexes.length; i++) {
+    var regionPattern = RegExp(regexes[i]);
+    var dnsSuffix = regionRegexes[regexes[i]];
+    if (regionPattern.test(region)) return dnsSuffix;
+  }
+  return defaultSuffix;
+}
+
 /**
  * @api private
  */
-module.exports = configureEndpoint;
+module.exports = {
+  configureEndpoint: configureEndpoint,
+  getEndpointSuffix: getEndpointSuffix
+};
diff --git a/lib/service.js b/lib/service.js
index f4c11c343..538c627e4 100644
--- a/lib/service.js
+++ b/lib/service.js
@@ -50,7 +50,7 @@ AWS.Service = inherit({
     if (config) this.config.update(config, true);
 
     this.validateService();
-    if (!this.config.endpoint) regionConfig(this);
+    if (!this.config.endpoint) regionConfig.configureEndpoint(this);
 
     this.config.endpoint = this.endpointFromTemplate(this.config.endpoint);
     this.setEndpoint(this.config.endpoint);
diff --git a/lib/services/s3.js b/lib/services/s3.js
index 5f745d7c5..22a911418 100644
--- a/lib/services/s3.js
+++ b/lib/services/s3.js
@@ -1,6 +1,7 @@
 var AWS = require('../core');
 var v4Credentials = require('../signers/v4_credentials');
 var resolveRegionalEndpointsFlag = require('../config_regional_endpoint');
+var regionUtil = require('../region_config');
 
 // Pull in managed upload extension
 require('../s3/managed_upload');
@@ -103,30 +104,40 @@ AWS.util.update(AWS.S3.prototype, {
   setupRequestListeners: function setupRequestListeners(request) {
     var prependListener = true;
     request.addListener('validate', this.validateScheme);
-    request.addListener('validate', this.validateBucketEndpoint);
-    request.addListener('validate', this.correctBucketRegionFromCache);
     request.addListener('validate', this.validateBucketName, prependListener);
     request.addListener('validate', this.optInUsEast1RegionalEndpoint, prependListener);
 
+    request.removeListener('validate',
+      AWS.EventListeners.Core.VALIDATE_REGION);
     request.addListener('build', this.addContentType);
-    request.addListener('build', this.populateURI);
     request.addListener('build', this.computeContentMd5);
     request.addListener('build', this.computeSseCustomerKeyMd5);
+    request.addListener('build', this.populateURI);
     request.addListener('afterBuild', this.addExpect100Continue);
-    request.removeListener('validate',
-      AWS.EventListeners.Core.VALIDATE_REGION);
     request.addListener('extractError', this.extractError);
-    request.onAsync('extractError', this.requestBucketRegion);
-    request.addListener('extractData', this.extractData);
     request.addListener('extractData', AWS.util.hoistPayloadMember);
+    request.addListener('extractData', this.extractData);
     request.addListener('beforePresign', this.prepareSignedUrl);
-    if (AWS.util.isBrowser()) {
-      request.onAsync('retry', this.reqRegionForNetworkingError);
-    }
     if (this.shouldDisableBodySigning(request))  {
       request.removeListener('afterBuild', AWS.EventListeners.Core.COMPUTE_SHA256);
       request.addListener('afterBuild', this.disableBodySigning);
     }
+    //deal with ARNs supplied to Bucket
+    if (this.isAccessPointApplicable(request)) {
+      request.removeListener('validate', this.validateBucketName);
+      request.addListener('validate', this.validateAccessPointArn, prependListener);
+      request.addListener('validate', this.validateArnRegion);
+      request.removeListener('build', this.populateURI);
+      request.addListener('build', this.populateUriFromAccessPoint);
+      return;
+    }
+    //listeners regarding region inference
+    request.addListener('validate', this.validateBucketEndpoint);
+    request.addListener('validate', this.correctBucketRegionFromCache);
+    request.onAsync('extractError', this.requestBucketRegion);
+    if (AWS.util.isBrowser()) {
+      request.onAsync('retry', this.reqRegionForNetworkingError);
+    }
   },
 
   /**
@@ -155,6 +166,156 @@ AWS.util.update(AWS.S3.prototype, {
     }
   },
 
+  /**
+   * @api private
+   */
+  isAccessPointApplicable: function hasBucketInParams(req) {
+    var inputShape = (req.service.api.operations[req.operation] || {}).input || {};
+    var inputMembers = inputShape.members || {};
+    if (
+      req.operation === 'createBucket' ||
+      !req.params.Bucket ||
+      !inputMembers.Bucket
+    ) return false;
+    if (!AWS.util.ARN.validate(req.params.Bucket)) return false;
+    return true;
+  },
+
+  /**
+   * Validate ARN supplied in Bucket parameter is a valid access point ARN
+   *
+   * @api private
+   */
+  validateAccessPointArn: function validateAccessPointArn(req) {
+    var parsedArn = AWS.util.ARN.parse(req.params.Bucket);
+    //avoid duplicated parsing in the future
+    req._parsedAccessPointArn = parsedArn;
+    var parsedArn = req._parsedAccessPointArn;
+    if (parsedArn.service !== 's3') {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidAccessPointARN',
+        message: 'expect \'s3\' in access point ARN service component'
+      });
+    }
+    if (!parsedArn.region) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidAccessPointARN',
+        message: 'Access point ARN region is empty'
+      });
+    }
+    if (
+      parsedArn.resource.indexOf('accesspoint:') !== 0 &&
+      parsedArn.resource.indexOf('accesspoint/') !== 0
+    ) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidAccessPointARN',
+        message: 'Access point ARN resource should begin with \'accesspoint/\''
+      });
+    }
+    var delimiter = parsedArn.resource['accesspoint'.length]; //can be ':' or '/'
+    if (parsedArn.resource.split(delimiter).length !== 2) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidAccessPointARN',
+        message: 'Too many resource parameters in access point ARN'
+      });
+    }
+    var accessPoint = parsedArn.resource.split(delimiter)[1];
+    var accessPointPrefix = accessPoint + '-' + parsedArn.accountId;
+    if (!req.service.isDnsCompatible(accessPointPrefix) || accessPointPrefix.match(/\./)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidAccessPointARN',
+        message: 'Access point ARN is not DNS compatible. Got ' + accessPoint
+      });
+    }
+    //set parsed valid access point
+    req._parsedAccessPointArn.accessPoint = accessPoint;
+  },
+
+  /**
+   * @api private
+   */
+  validateArnRegion: function validateArnRegion(req) {
+    var useArnRegion = req.service.loadUseArnRegionConfig(req);
+    var regionFromArn = req._parsedAccessPointArn.region;
+    var clientRegion = req.service.config.region;
+    if (
+      clientRegion.indexOf('fips') >= 0 ||
+      regionFromArn.indexOf('fips') >= 0
+    ) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'Access point endpoint is not compatible with FIPS region'
+      });
+    }
+    if (!useArnRegion && regionFromArn !== clientRegion) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'Configured region conflicts with access point region'
+      });
+    } else if (
+      useArnRegion &&
+      regionUtil.getEndpointSuffix(regionFromArn) !== regionUtil.getEndpointSuffix(clientRegion)
+    ) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'Configured region and access point region not in same partition'
+      });
+    }
+    if (req.service.config.useAccelerateEndpoint) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'useAccelerateEndpoint config is not supported with access point ARN'
+      });
+    }
+  },
+
+  /**
+   * @api private
+   */
+  loadUseArnRegionConfig: function loadUseArnRegionConfig(req) {
+    var envName = 'AWS_S3_USE_ARN_REGION';
+    var configName = 's3_use_arn_region';
+    var useArnRegion = true;
+    var originalConfig = req.service._originalConfig || {};
+    if (req.service.config.s3UseArnRegion !== undefined) {
+      return req.service.config.s3UseArnRegion;
+    } else if (originalConfig.s3UseArnRegion !== undefined) {
+      useArnRegion = originalConfig.s3UseArnRegion === true;
+    } else if (AWS.util.isNode()) {
+      //load from environmental variable AWS_USE_ARN_REGION
+      if (process.env[envName]) {
+        var value = process.env[envName].trim().toLowerCase();
+        if (['false', 'true'].indexOf(value) < 0) {
+          throw AWS.util.error(new Error(), {
+            code: 'InvalidConfiguration',
+            message: envName + ' only accepts true or false. Got ' + process.env[envName],
+            retryable: false
+          });
+        }
+        useArnRegion = value === 'true';
+      } else {  //load from shared config property use_arn_region
+        var profiles = {};
+        var profile = {};
+        try {
+          profiles = AWS.util.getProfilesFromSharedConfig(AWS.util.iniLoader);
+          profile = profiles[process.env.AWS_PROFILE || AWS.util.defaultProfile];
+        } catch (e) {}
+        if (profile[configName]) {
+          if (['false', 'true'].indexOf(profile[configName].trim().toLowerCase()) < 0) {
+            throw AWS.util.error(new Error(), {
+              code: 'InvalidConfiguration',
+              message: configName + ' only accepts true or false. Got ' + profile[configName],
+              retryable: false
+            });
+          }
+          useArnRegion = profile[configName].trim().toLowerCase() === 'true';
+        }
+      }
+    }
+    req.service.config.s3UseArnRegion = useArnRegion;
+    return useArnRegion;
+  },
+
   /**
    * @api private
    */
@@ -283,6 +444,45 @@ AWS.util.update(AWS.S3.prototype, {
     }
   },
 
+  /**
+   * When user supply an access point ARN in the Bucket parameter, we need to
+   * populate the URI according to the ARN.
+   * @api private
+   */
+  populateUriFromAccessPoint: function populateUriFromAccessPoint(req) {
+    if (req.service._originalConfig.endpoint) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'Custom endpoint is not compatible with access point ARN'
+      });
+    }
+    if (req.service.config.s3ForcePathStyle) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'Cannot construct path-style endpoint with access point'
+      });
+    }
+    var accessPointArn = req._parsedAccessPointArn;
+    var serviceName = req.service.config.useDualstack ?
+      's3-accesspoint.dualstack':
+      's3-accesspoint';
+    var endpoint = req.httpRequest.endpoint;
+    var dnsSuffix = regionUtil.getEndpointSuffix(accessPointArn.region);
+    var useArnRegion = req.service.config.s3UseArnRegion;
+    endpoint.hostname = [
+      accessPointArn.accessPoint + '-' + accessPointArn.accountId,
+      serviceName,
+      useArnRegion ? accessPointArn.region : req.service.config.region,
+      dnsSuffix
+    ].join('.');
+    endpoint.host = endpoint.hostname;
+    var encodedArn = AWS.util.uriEscapePath(req.params.Bucket);
+    var path = req.httpRequest.path;
+    //remove the Bucket value from path
+    req.httpRequest.path = path.replace(new RegExp('/' + encodedArn), '');
+    req.httpRequest.region = accessPointArn.region; //region used to sign
+  },
+
   /**
    * Adds Expect: 100-continue header if payload is greater-or-equal 1MB
    * @api private
@@ -419,7 +619,7 @@ AWS.util.update(AWS.S3.prototype, {
     if (this.config.s3ForcePathStyle) return true;
     if (this.config.s3BucketEndpoint) return false;
 
-    if (this.dnsCompatibleBucketName(bucketName)) {
+    if (this.isDnsCompatible(bucketName)) {
       return (this.config.sslEnabled && bucketName.match(/\./)) ? true : false;
     } else {
       return true; // not dns compatible names must always use path style
@@ -432,7 +632,7 @@ AWS.util.update(AWS.S3.prototype, {
    *
    * @api private
    */
-  dnsCompatibleBucketName: function dnsCompatibleBucketName(bucketName) {
+  isDnsCompatible: function isDnsCompatible(bucketName) {
     var b = bucketName;
     var domain = new RegExp(/^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$/);
     var ipAddress = new RegExp(/(\d+\.){3}\d+/);
@@ -674,7 +874,7 @@ AWS.util.update(AWS.S3.prototype, {
     if (cachedRegion && cachedRegion !== request.httpRequest.region) {
       service.updateReqBucketRegion(request, cachedRegion);
       done();
-    } else if (!service.dnsCompatibleBucketName(bucket)) {
+    } else if (!service.isDnsCompatible(bucket)) {
       service.updateReqBucketRegion(request, 'us-east-1');
       if (bucketRegionCache[bucket] !== 'us-east-1') {
         bucketRegionCache[bucket] = 'us-east-1';
diff --git a/lib/util.js b/lib/util.js
index 802f23134..323417152 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -975,6 +975,35 @@ var util = {
     return profiles;
   },
 
+  /**
+   * @api private
+   */
+  ARN: {
+    validate: function validateARN(str) {
+      return str && str.indexOf('arn:') === 0 && str.split(':').length >= 6;
+    },
+    parse: function parseARN(arn) {
+      var matched = arn.split(':');
+      return {
+        partition: matched[1],
+        service: matched[2],
+        region: matched[3],
+        accountId: matched[4],
+        resource: matched.slice(5).join(':')
+      };
+    },
+    build: function buildARN(arnObject) {
+      if (
+        arnObject.service === undefined ||
+        arnObject.region === undefined ||
+        arnObject.accountId === undefined ||
+        arnObject.resource === undefined
+      ) throw util.error(new Error('Input ARN object is invalid'));
+      return 'arn:'+ (arnObject.partition || 'aws') + ':' + arnObject.service +
+        ':' + arnObject.region + ':' + arnObject.accountId + ':' + arnObject.resource;
+    }
+  },
+
   /**
    * @api private
    */
diff --git a/scripts/region-checker/whitelist.js b/scripts/region-checker/whitelist.js
index 2d542b7f0..98e1f9955 100644
--- a/scripts/region-checker/whitelist.js
+++ b/scripts/region-checker/whitelist.js
@@ -4,9 +4,9 @@ var whitelist = {
         25,
         85,
         86,
-        193,
-        247,
-        248
+        197,
+        251,
+        252
     ],
     '/credentials/cognito_identity_credentials.js': [
         78,
@@ -29,20 +29,19 @@ var whitelist = {
         316
     ],
     '/services/s3.js': [
-        69,
         70,
-        194,
-        196,
-        209,
-        215,
-        545,
-        546,
-        547,
-        666,
-        677,
-        678,
-        684,
-        679
+        71,
+        355,
+        357,
+        370,
+        376,
+        745,
+        747,
+        866,
+        877,
+        878,
+        879,
+        884
     ]
 };
 


Test patch:
diff --git a/test/services/s3.spec.js b/test/services/s3.spec.js
index 95ac99748..f933b9e00 100644
--- a/test/services/s3.spec.js
+++ b/test/services/s3.spec.js
@@ -21,42 +21,42 @@ describe('AWS.S3', function() {
     return done();
   });
 
-  describe('dnsCompatibleBucketName', function() {
+  describe('isDnsCompatible', function() {
     it('must be at least 3 characters', function() {
-      expect(s3.dnsCompatibleBucketName('aa')).to.equal(false);
+      expect(s3.isDnsCompatible('aa')).to.equal(false);
     });
 
     it('must not be longer than 63 characters', function() {
       var b;
       b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
-      expect(s3.dnsCompatibleBucketName(b)).to.equal(false);
+      expect(s3.isDnsCompatible(b)).to.equal(false);
     });
 
     it('must start with a lower-cased letter or number', function() {
-      expect(s3.dnsCompatibleBucketName('Abc')).to.equal(false);
-      expect(s3.dnsCompatibleBucketName('-bc')).to.equal(false);
-      expect(s3.dnsCompatibleBucketName('abc')).to.equal(true);
+      expect(s3.isDnsCompatible('Abc')).to.equal(false);
+      expect(s3.isDnsCompatible('-bc')).to.equal(false);
+      expect(s3.isDnsCompatible('abc')).to.equal(true);
     });
 
     it('must end with a lower-cased letter or number', function() {
-      expect(s3.dnsCompatibleBucketName('abC')).to.equal(false);
-      expect(s3.dnsCompatibleBucketName('ab-')).to.equal(false);
-      expect(s3.dnsCompatibleBucketName('abc')).to.equal(true);
+      expect(s3.isDnsCompatible('abC')).to.equal(false);
+      expect(s3.isDnsCompatible('ab-')).to.equal(false);
+      expect(s3.isDnsCompatible('abc')).to.equal(true);
     });
 
     it('may not contain multiple contiguous dots', function() {
-      expect(s3.dnsCompatibleBucketName('abc.123')).to.equal(true);
-      expect(s3.dnsCompatibleBucketName('abc..123')).to.equal(false);
+      expect(s3.isDnsCompatible('abc.123')).to.equal(true);
+      expect(s3.isDnsCompatible('abc..123')).to.equal(false);
     });
 
     it('may only contain letters numbers and dots', function() {
-      expect(s3.dnsCompatibleBucketName('abc123')).to.equal(true);
-      expect(s3.dnsCompatibleBucketName('abc_123')).to.equal(false);
+      expect(s3.isDnsCompatible('abc123')).to.equal(true);
+      expect(s3.isDnsCompatible('abc_123')).to.equal(false);
     });
 
     it('must not look like an ip address', function() {
-      expect(s3.dnsCompatibleBucketName('1.2.3.4')).to.equal(false);
-      expect(s3.dnsCompatibleBucketName('a.b.c.d')).to.equal(true);
+      expect(s3.isDnsCompatible('1.2.3.4')).to.equal(false);
+      expect(s3.isDnsCompatible('a.b.c.d')).to.equal(true);
     });
   });
 
@@ -2928,6 +2928,18 @@ describe('AWS.S3', function() {
       });
       invocationDeferred = true;
     });
+
+    it('supports access point ARN', function(done) {
+      s3.getSignedUrl('getObject', {
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      }, function(err, data) {
+        expect(data).to.equal(
+          'https://myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com/key?AWSAccessKeyId=akid&Expires=900&Signature=R9HjMWdhk69e7%2BwlOpFH1TUkxRY%3D&x-amz-security-token=session'
+        );
+        done();
+      });
+    });
   });
   describe('getSignedUrlPromise', function() {
     var catchFunction, resolveFunction, err, url, date;
@@ -3332,4 +3344,344 @@ describe('AWS.S3', function() {
       });
     }
   });
+
+  describe('Access Point', function() {
+    var iniLoader = AWS.util.iniLoader;
+    afterEach(function() {
+      if (AWS.util.isNode()) {
+        iniLoader.clearCachedFiles();
+      }
+    });
+
+    it('should correctly generate access point endpoint', function(done) {
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.not.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal(
+          'myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com'
+        );
+        expect(request.httpRequest.path).to.equal('/key');
+        done();
+      });
+    });
+
+    it('should correctly generate access point endpoint for us-gov partition', function(done) {
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws-us-gov:s3:us-gov-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal(
+          'myendpoint-123456789012.s3-accesspoint.us-gov-east-1.amazonaws.com'
+        );
+        expect(request.httpRequest.path).to.equal('/key');
+        done();
+      });
+    });
+
+    it('should use endpoint suffix according to endpoint partition(China)', function(done) {
+      s3 = new AWS.S3({region: 'cn-north-1'});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws-cn:s3:cn-north-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.not.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal('myendpoint-123456789012.s3-accesspoint.cn-north-1.amazonaws.com.cn');
+        expect(request.httpRequest.path).to.equal('/key');
+        done();
+      });
+    });
+
+    it('should correctly generate access point endpoint for pseudo regions', function() {
+      s3 = new AWS.S3({region: 'us-east-1'});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:s3-external-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      var built = request.build(function() {});
+      expect(
+        built.httpRequest.endpoint.hostname
+      ).to.equal('myendpoint-123456789012.s3-accesspoint.s3-external-1.amazonaws.com');
+
+      s3 = new AWS.S3({region: 'us-east-1-fips'});
+      helpers.mockHttpResponse(200, {}, '');
+      request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      var error;
+      request.build(function(err) {
+        error = err;
+      });
+      expect(error.name).to.equal('InvalidConfiguration');
+      expect(error.message).to.equal('Access point endpoint is not compatible with FIPS region');
+    });
+
+    it('should use regions from ARN by default', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2'});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.not.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal('myendpoint-123456789012.s3-accesspoint.us-east-1.amazonaws.com');
+        expect(request.httpRequest.path).to.equal('/key');
+        done();
+      });
+    });
+
+    it('should not use regions from ARN if s3UseArnRegion config is set to false', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2', s3UseArnRegion: false});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidConfiguration');
+        expect(err.message).to.equal('Configured region conflicts with access point region');
+        done();
+      });
+    });
+
+    it('should throw if manually-set endpoint is present', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2', endpoint: 'https://bucket.s3.us-west-2.amazonaws.com'});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidConfiguration');
+        expect(err.message).to.equal('Custom endpoint is not compatible with access point ARN');
+        done();
+      });
+    });
+
+    it('should throw if s3forcePathStyle co-exists with access point ARN', function(done) {
+      s3 = new AWS.S3({s3ForcePathStyle: true});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidConfiguration');
+        expect(err.message).to.equal('Cannot construct path-style endpoint with access point');
+        done();
+      });
+    });
+
+    it('should not apply access point ARN parsing to CreateBucketAPI', function(done) {
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.createBucket({
+        Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint'
+      });
+      request.send(function(err, data) {
+        expect(err).to.not.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal('s3.amazonaws.com');
+        expect(request.httpRequest.path).to.equal('/arn%3Aaws%3As3%3Aus-east-1%3A123456789012%3Aaccesspoint%3Amyendpoint');
+        done();
+      });
+    });
+
+    it('should throw if cross region enabled but region from different partition', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2'});
+      var request = s3.getObject({
+        Bucket: 'arn:aws-cn:s3:cn-north-1:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidConfiguration');
+        expect(err.message).to.equal('Configured region and access point region not in same partition');
+        done();
+      });
+    });
+
+    if (AWS.util.isNode()) {
+      it('should throw if AWS_S3_USE_ARN_REGION env is invalid', function(done) {
+        process.env.AWS_S3_USE_ARN_REGION = 'foo';
+        s3 = new AWS.S3({region: 'us-west-2'});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('AWS_S3_USE_ARN_REGION only accepts true or false. Got foo');
+          done();
+        });
+      });
+
+      it('should use regions from ARN if AWS_S3_USE_ARN_REGION env is set', function(done) {
+        process.env.AWS_S3_USE_ARN_REGION = 'false';
+        s3 = new AWS.S3({region: 'us-west-2'});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('Configured region conflicts with access point region');
+          done();
+        });
+      });
+
+      it('should throw s3_use_arn_region config is invalid', function(done) {
+        var mock = '[default]\ns3_use_arn_region = foo';
+        process.env.HOME = 'foo';
+        helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock);
+        s3 = new AWS.S3({region: 'us-west-2'});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('s3_use_arn_region only accepts true or false. Got foo');
+          done();
+        });
+      });
+
+      it('should use regions from ARN if s3_use_arn_region config is set', function(done) {
+        var mock = '[default]\ns3_use_arn_region = false';
+        process.env.HOME = 'foo';
+        helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock);
+        s3 = new AWS.S3({region: 'us-west-2'});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('Configured region conflicts with access point region');
+          done();
+        });
+      });
+    }
+
+    it('should correctly generate accelerate endpoint from access point arn', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2', useAccelerateEndpoint: true});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidConfiguration');
+        expect(err.message).to.equal('useAccelerateEndpoint config is not supported with access point ARN');
+        done();
+      });
+    });
+
+    it('should correctly generate dualstack endpoint from access point arn', function(done) {
+      s3 = new AWS.S3({region: 'us-west-2', useDualstack: true});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.not.exist;
+        expect(request.httpRequest.endpoint.hostname).to.equal('myendpoint-123456789012.s3-accesspoint.dualstack.us-west-2.amazonaws.com');
+        expect(request.httpRequest.path).to.equal('/key');
+        done();
+      });
+    });
+
+    it('should throw if supplied non-s3 ARN', function(done) {
+      s3 = new AWS.S3();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:sqs:us-west-2:123456789012:someresource',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidAccessPointARN');
+        expect(err.message).to.equal('expect \'s3\' in access point ARN service component');
+        done();
+      });
+    });
+
+    it('should throw if access point ARN is not for access point resournce', function(done) {
+      s3 = new AWS.S3();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:bucket_name:mybucket',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidAccessPointARN');
+        expect(err.message).to.equal('Access point ARN resource should begin with \'accesspoint/\'');
+        done();
+      });
+    });
+
+    it('should throw if access point is not dns compatible', function(done) {
+      s3 = new AWS.S3();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:my.bucket',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidAccessPointARN');
+        expect(err.message).to.equal('Access point ARN is not DNS compatible. Got my.bucket');
+        done();
+      });
+    });
+
+    it('should throw if access point ARN missing components', function(done) {
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3::123456789012:accesspoint:mybucket',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidAccessPointARN');
+        done();
+      });
+    });
+
+    it('should throw if access point ARN has extra resource component', function(done) {
+      helpers.mockHttpResponse(200, {}, '');
+      var request = s3.getObject({
+        Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:mybucket:object:foo',
+        Key: 'key'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidAccessPointARN');
+        done();
+      });
+    });
+  });
 });
diff --git a/test/util.spec.js b/test/util.spec.js
index d0d578435..248d7e8a3 100644
--- a/test/util.spec.js
+++ b/test/util.spec.js
@@ -1716,4 +1716,72 @@
     });
   }
 
+  describe('AWS.utils.ARN', function() {
+    describe('validate', function() {
+      it('should validate whether input is a qualified resource ARN', function() {
+        expect(AWS.util.ARN.validate('arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint')).to.equal(true);
+        expect(AWS.util.ARN.validate('arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint')).to.equal(true);
+        expect(AWS.util.ARN.validate('arn:aws-cn:s3:cn-north-1:123456789012:accesspoint:myendpoint')).to.equal(true);
+        expect(AWS.util.ARN.validate('arn:aws:sns:us-west-2:123456789012:myTopic')).to.equal(true);
+      });
+    });
+
+    describe('parser', function() {
+      it('should parse valid resource ARNs', function() {
+        expect(AWS.util.ARN.parse('arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint')).to.contain({
+          partition: 'aws',
+          service: 's3',
+          region: 'us-west-2',
+          accountId: '123456789012',
+          resource: 'accesspoint:myendpoint'
+        });
+        expect(AWS.util.ARN.parse('arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint')).to.contain({
+          partition: 'aws',
+          service: 's3',
+          region: 'us-east-1',
+          accountId: '123456789012',
+          resource: 'accesspoint:myendpoint'
+        });
+        expect(AWS.util.ARN.parse('arn:aws-cn:s3:cn-north-1:123456789012:accesspoint:myendpoint')).to.contain({
+          partition: 'aws-cn',
+          service: 's3',
+          region: 'cn-north-1',
+          accountId: '123456789012',
+          resource: 'accesspoint:myendpoint'
+        });
+        expect(AWS.util.ARN.parse('arn:aws:sns:us-west-2:123456789012:myTopic')).to.contain({
+          partition: 'aws',
+          service: 'sns',
+          region: 'us-west-2',
+          accountId: '123456789012',
+          resource: 'myTopic'
+        });
+      });
+    });
+
+    describe('builder', function() {
+      it('should build valid ARN object to string', function() {
+        expect(AWS.util.ARN.build({
+          service: 's3',
+          region: 'us-east-1',
+          accountId: '123456789012',
+          resource: 'accesspoint:myendpoint'
+        })).to.equal('arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint');
+      });
+
+      it('should throw if required ARN component is missing', function(done) {
+        try {
+          AWS.util.ARN.build({
+            service: 's3',
+            region: 'us-east-1',
+            resource: 'accesspoint:myendpoint'
+          });
+        } catch (e) {
+          expect(e.message).to.equal('Input ARN object is invalid');
+          done();
+        }
+      });
+    });
+  });
+
 }).call(this);