Repository: aws/aws-sdk-js
Pull request #3471 (2020-09-30 15:38:36)

Support S3 on Outposts Access Point and Bucket ARNs
Internal issue JS-1927

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] `npm run test` passes
- [ ] `.d.ts` file is updated
- [x] changelog is added, `npm run add-change`
- [ ] run `bundle exec rake docs:api` and inspect `doc/latest/index.html` if documentation is changed
- [ ] run `npm run integration` if integration test is changed
- [ ] non-code related change (markdown/git settings etc)


aws-sdk-js-automation: 
### AWS CodeBuild CI Report

* CodeBuild project: sdk-v2-github
* Commit ID: ec7d87f562f99f1078b2c4c0aa5e6b21bc4a06ff
* Result: SUCCEEDED
* [Build Logs](https://r6vdd33du7.execute-api.us-west-2.amazonaws.com/Prod/buildlogs?key=01cf4ea1-1f96-4112-8924-e5f2bc28026c%2Fbuild.log) (available for 30 days)

*Powered by [github-codebuild-logs](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:277187709615:applications~github-codebuild-logs), available on the [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/)*


<!--
CREATED BY GITHUB-CODEBUILD-LOGS
-->


trivikr: The models updated was reverted from PR as they will be updated during the release.
This branch just pushes the required customizations.

Patch:
diff --git a/.changes/next-release/feature-S3-9201f5eb.json b/.changes/next-release/feature-S3-9201f5eb.json
new file mode 100644
index 000000000..376880f4c
--- /dev/null
+++ b/.changes/next-release/feature-S3-9201f5eb.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "S3",
+  "description": "Support S3 on Outposts Access Point and Bucket ARNs"
+}
\ No newline at end of file
diff --git a/lib/event_listeners.js b/lib/event_listeners.js
index a56784b04..ba22ab8ea 100644
--- a/lib/event_listeners.js
+++ b/lib/event_listeners.js
@@ -225,7 +225,7 @@ AWS.EventListeners = {
           var date = service.getSkewCorrectedDate();
           var SignerClass = service.getSignerClass(req);
           var signer = new SignerClass(req.httpRequest,
-            service.api.signingName || service.api.endpointPrefix,
+            service.getSigningName(),
             {
               signatureCache: service.config.signatureCache,
               operation: operation,
diff --git a/lib/service.js b/lib/service.js
index f6b4f52b6..0c1c519eb 100644
--- a/lib/service.js
+++ b/lib/service.js
@@ -454,6 +454,14 @@ AWS.Service = inherit({
   setupRequestListeners: function setupRequestListeners(request) {
   },
 
+  /**
+   * Gets the signing name for a given request
+   * @api private
+   */
+  getSigningName: function getSigningName() {
+    return this.api.signingName || this.api.endpointPrefix;
+  },
+
   /**
    * Gets the signer class for a given request
    * @api private
diff --git a/lib/services/s3.js b/lib/services/s3.js
index cf593259a..345f45ef5 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 s3util = require('./s3util');
 var regionUtil = require('../region_config');
 
 // Pull in managed upload extension
@@ -53,6 +54,16 @@ AWS.util.update(AWS.S3.prototype, {
     return defaultApiVersion;
   },
 
+  /**
+   * @api private
+   */
+  getSigningName: function getSigningName() {
+    var _super = AWS.Service.prototype.getSigningName;
+    return (this._parsedArn && this._parsedArn.service)
+      ? this._parsedArn.service
+      : _super.call(this);
+  },
+
   /**
    * @api private
    */
@@ -124,12 +135,24 @@ AWS.util.update(AWS.S3.prototype, {
       request.addListener('afterBuild', this.disableBodySigning);
     }
     //deal with ARNs supplied to Bucket
-    if (this.isAccessPointApplicable(request)) {
+    if (request.operation !== 'createBucket' && s3util.isArnInParam(request, 'Bucket')) {
+      // avoid duplicate parsing in the future
+      request.service._parsedArn = AWS.util.ARN.parse(request.params.Bucket);
+
       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);
+      if (request.service._parsedArn.service === 's3') {
+        request.addListener('validate', s3util.validateS3AccessPointArn);
+        request.addListener('validate', this.validateArnResourceType);
+      } else if (request.service._parsedArn.service === 's3-outposts') {
+        request.addListener('validate', s3util.validateOutpostsAccessPointArn);
+        request.addListener('validate', s3util.validateOutpostsArn);
+      }
+      request.addListener('validate', s3util.validateArnRegion);
+      request.addListener('validate', s3util.validateArnAccount);
+      request.addListener('validate', s3util.validateArnService);
+      request.addListener('build', this.populateUriFromAccessPointArn);
+      request.addListener('build', s3util.validatePopulateUriFromArn);
       return;
     }
     //listeners regarding region inference
@@ -168,161 +191,22 @@ AWS.util.update(AWS.S3.prototype, {
   },
 
   /**
-   * @api private
+   * Validate resource-type supplied in S3 ARN
    */
-  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 (!/[0-9]{12}/.exec(parsedArn.accountId)) {
-      throw AWS.util.error(new Error(), {
-        code: 'InvalidAccessPointARN',
-        message: 'Access point ARN accountID does not match regex "[0-9]{12}"'
-      });
-    }
-    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.dnsCompatibleBucketName(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;
-  },
+  validateArnResourceType: function validateArnResourceType(req) {
+    var resource = req.service._parsedArn.resource;
 
-  /**
-   * @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)
+      resource.indexOf('accesspoint:') !== 0 &&
+      resource.indexOf('accesspoint/') !== 0
     ) {
       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'
+        code: 'InvalidARN',
+        message: 'ARN resource should begin with \'accesspoint/\''
       });
     }
   },
 
-  /**
-   * @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
    */
@@ -452,35 +336,28 @@ 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 && 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';
+  populateUriFromAccessPointArn: function populateUriFromAccessPointArn(req) {
+    var accessPointArn = req.service._parsedArn;
+
+    var isOutpostArn = accessPointArn.service === 's3-outposts';
+
+    var outpostsSuffix = isOutpostArn ? '.' + accessPointArn.outpostId: '';
+    var serviceName = isOutpostArn ? 's3-outposts': 's3-accesspoint';
+    var dualStackSuffix = !isOutpostArn && req.service.config.useDualstack ? '.dualstack' : '';
+
     var endpoint = req.httpRequest.endpoint;
     var dnsSuffix = regionUtil.getEndpointSuffix(accessPointArn.region);
     var useArnRegion = req.service.config.s3UseArnRegion;
+
     endpoint.hostname = [
-      accessPointArn.accessPoint + '-' + accessPointArn.accountId,
-      serviceName,
+      accessPointArn.accessPoint + '-' + accessPointArn.accountId + outpostsSuffix,
+      serviceName + dualStackSuffix,
       useArnRegion ? accessPointArn.region : req.service.config.region,
       dnsSuffix
     ].join('.');
     endpoint.host = endpoint.hostname;
+
     var encodedArn = AWS.util.uriEscape(req.params.Bucket);
     var path = req.httpRequest.path;
     //remove the Bucket value from path
@@ -627,27 +504,13 @@ AWS.util.update(AWS.S3.prototype, {
     if (this.config.s3ForcePathStyle) return true;
     if (this.config.s3BucketEndpoint) return false;
 
-    if (this.dnsCompatibleBucketName(bucketName)) {
+    if (s3util.dnsCompatibleBucketName(bucketName)) {
       return (this.config.sslEnabled && bucketName.match(/\./)) ? true : false;
     } else {
       return true; // not dns compatible names must always use path style
     }
   },
 
-  /**
-   * Returns true if the bucket name is DNS compatible.  Buckets created
-   * outside of the classic region MUST be DNS compatible.
-   *
-   * @api private
-   */
-  dnsCompatibleBucketName: function dnsCompatibleBucketName(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+/);
-    var dots = new RegExp(/\.\./);
-    return (b.match(domain) && !b.match(ipAddress) && !b.match(dots)) ? true : false;
-  },
-
   /**
    * For COPY operations, some can be error even with status code 200.
    * SDK treats the response as exception when response body indicates
@@ -896,7 +759,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 (!s3util.dnsCompatibleBucketName(bucket)) {
       service.updateReqBucketRegion(request, 'us-east-1');
       if (bucketRegionCache[bucket] !== 'us-east-1') {
         bucketRegionCache[bucket] = 'us-east-1';
diff --git a/lib/services/s3control.js b/lib/services/s3control.js
index b24bc83db..273b7db8d 100644
--- a/lib/services/s3control.js
+++ b/lib/services/s3control.js
@@ -1,35 +1,128 @@
 var AWS = require('../core');
+var s3util = require('./s3util');
+var regionUtil = require('../region_config');
 
 AWS.util.update(AWS.S3Control.prototype, {
   /**
    * @api private
    */
   setupRequestListeners: function setupRequestListeners(request) {
-    request.addListener('afterBuild', this.prependAccountId);
     request.addListener('extractError', this.extractHostId);
     request.addListener('extractData', this.extractHostId);
     request.addListener('validate', this.validateAccountId);
+
+    var isArnInBucket = s3util.isArnInParam(request, 'Bucket');
+    var isArnInName = s3util.isArnInParam(request, 'Name');
+
+    if (isArnInBucket) {
+      request.service._parsedArn = AWS.util.ARN.parse(request.params['Bucket']);
+      request.service.signingName = request.service._parsedArn.service;
+      request.addListener('validate', this.validateOutpostsBucketArn);
+      request.addListener('validate', s3util.validateOutpostsArn);
+      request.addListener('afterBuild', this.addOutpostIdHeader);
+    } else if (isArnInName) {
+      request.service._parsedArn = AWS.util.ARN.parse(request.params['Name']);
+      request.service.signingName = request.service._parsedArn.service;
+      request.addListener('validate', s3util.validateOutpostsAccessPointArn);
+      request.addListener('validate', s3util.validateOutpostsArn);
+      request.addListener('afterBuild', this.addOutpostIdHeader);
+    }
+
+    if (isArnInBucket || isArnInName) {
+      request.addListener('validate', s3util.validateArnRegion);
+      request.addListener('validate', this.validateArnAccountWithParams, true);
+      request.addListener('validate', s3util.validateArnAccount);
+      request.addListener('validate', s3util.validateArnService);
+      request.addListener('build', this.populateParamFromArn, true);
+      request.addListener('build', this.populateUriFromArn);
+      request.addListener('build', s3util.validatePopulateUriFromArn);
+    }
+
+    if (request.params.OutpostId &&
+        (request.operation === 'createBucket' ||
+          request.operation === 'listRegionalBuckets')) {
+      request.service.signingName = 's3-outposts';
+      request.addListener('build', this.populateEndpointForOutpostId);
+    }
+  },
+
+  /**
+   * Adds outpostId header
+   */
+  addOutpostIdHeader: function addOutpostIdHeader(req) {
+    req.httpRequest.headers['x-amz-outpost-id'] = req.service._parsedArn.outpostId;
+  },
+
+  /**
+   * Validate Outposts ARN supplied in Bucket parameter is a valid bucket name
+   */
+  validateOutpostsBucketArn: function validateOutpostsBucketArn(req) {
+    var parsedArn = req.service._parsedArn;
+
+    //can be ':' or '/'
+    var delimiter = parsedArn.resource['outpost'.length];
+
+    if (parsedArn.resource.split(delimiter).length !== 4) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Bucket ARN should have two resources outpost/{outpostId}/bucket/{accesspointName}'
+      });
+    }
+
+    var bucket = parsedArn.resource.split(delimiter)[3];
+    if (!s3util.dnsCompatibleBucketName(bucket) || bucket.match(/\./)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Bucket ARN is not DNS compatible. Got ' + bucket
+      });
+    }
+
+    //set parsed valid bucket
+    req.service._parsedArn.bucket = bucket;
   },
 
   /**
    * @api private
    */
-  prependAccountId: function(request) {
-    var api = request.service.api;
-    var operationModel = api.operations[request.operation];
-    var inputModel = operationModel.input;
-    var params = request.params;
-    if (inputModel.members.AccountId && params.AccountId) {
-      //customization needed
-      var accountId = params.AccountId;
-      var endpoint = request.httpRequest.endpoint;
-      var newHostname = String(accountId) + '.' + endpoint.hostname;
-      endpoint.hostname = newHostname;
-      request.httpRequest.headers.Host = newHostname;
-      delete request.httpRequest.headers['x-amz-account-id'];
+  populateParamFromArn: function populateParamFromArn(req) {
+    var parsedArn = req.service._parsedArn;
+    if (s3util.isArnInParam(req, 'Bucket')) {
+      req.params.Bucket = parsedArn.bucket;
+    } else if (s3util.isArnInParam(req, 'Name')) {
+      req.params.Name = parsedArn.accessPoint;
     }
   },
 
+  /**
+   * Populate URI according to the ARN
+   */
+  populateUriFromArn: function populateUriFromArn(req) {
+    var parsedArn = req.service._parsedArn;
+
+    var endpoint = req.httpRequest.endpoint;
+    var useArnRegion = req.service.config.s3UseArnRegion;
+
+    endpoint.hostname = [
+      's3-outposts',
+      useArnRegion ? parsedArn.region : req.service.config.region,
+      'amazonaws.com'
+    ].join('.');
+    endpoint.host = endpoint.hostname;
+  },
+
+  /**
+   * @api private
+   */
+  populateEndpointForOutpostId: function populateEndpointForOutpostId(req) {
+    var endpoint = req.httpRequest.endpoint;
+    endpoint.hostname = [
+      's3-outposts',
+      req.service.config.region,
+      'amazonaws.com'
+    ].join('.');
+    endpoint.host = endpoint.hostname;
+  },
+
   /**
    * @api private
    */
@@ -41,6 +134,30 @@ AWS.util.update(AWS.S3Control.prototype, {
     }
   },
 
+  /**
+   * @api private
+   */
+  validateArnAccountWithParams: function validateArnAccountWithParams(req) {
+    var params = req.params;
+    var inputModel = req.service.api.operations[req.operation].input;
+    if (inputModel.members.AccountId) {
+      var parsedArn = req.service._parsedArn;
+      if (parsedArn.accountId) {
+        if (params.AccountId) {
+          if (params.AccountId !== parsedArn.accountId) {
+            throw AWS.util.error(
+              new Error(),
+              {code: 'ValidationError', message: 'AccountId in ARN and request params should be same.'}
+            );
+          }
+        } else {
+          // Store accountId from ARN in params
+          params.AccountId = parsedArn.accountId;
+        }
+      }
+    }
+  },
+
   /**
    * @api private
    */
@@ -68,5 +185,15 @@ AWS.util.update(AWS.S3Control.prototype, {
       throw AWS.util.error(new Error(),
         {code: 'ValidationError', message: 'AccountId should be hostname compatible. AccountId: ' + accountId});
     }
-  }
+  },
+
+  /**
+   * @api private
+   */
+  getSigningName: function getSigningName() {
+    var _super = AWS.Service.prototype.getSigningName;
+    return (this.signingName)
+      ? this.signingName
+      : _super.call(this);
+  },
 });
diff --git a/lib/services/s3util.js b/lib/services/s3util.js
new file mode 100644
index 000000000..713fe227f
--- /dev/null
+++ b/lib/services/s3util.js
@@ -0,0 +1,266 @@
+var AWS = require('../core');
+var regionUtil = require('../region_config');
+
+var s3util = {
+  /**
+   * @api private
+   */
+  isArnInParam: function isArnInParam(req, paramName) {
+    var inputShape = (req.service.api.operations[req.operation] || {}).input || {};
+    var inputMembers = inputShape.members || {};
+    if (!req.params[paramName] || !inputMembers[paramName]) return false;
+    return AWS.util.ARN.validate(req.params[paramName]);
+  },
+
+  /**
+   * Validate service component from ARN supplied in Bucket parameter
+   */
+  validateArnService: function validateArnService(req) {
+    var parsedArn = req.service._parsedArn;
+
+    if (parsedArn.service !== 's3' && parsedArn.service !== 's3-outposts') {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'expect \'s3\' or \'s3-outposts\' in ARN service component'
+      });
+    }
+  },
+
+  /**
+   * Validate account ID from ARN supplied in Bucket parameter is a valid account
+   */
+  validateArnAccount: function validateArnAccount(req) {
+    var parsedArn = req.service._parsedArn;
+
+    if (!/[0-9]{12}/.exec(parsedArn.accountId)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'ARN accountID does not match regex "[0-9]{12}"'
+      });
+    }
+  },
+
+  /**
+   * Validate ARN supplied in Bucket parameter is a valid access point ARN
+   */
+  validateS3AccessPointArn: function validateS3AccessPointArn(req) {
+    var parsedArn = req.service._parsedArn;
+
+    //can be ':' or '/'
+    var delimiter = parsedArn.resource['accesspoint'.length];
+
+    if (parsedArn.resource.split(delimiter).length !== 2) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Access Point ARN should have one resource accesspoint/{accesspointName}'
+      });
+    }
+
+    var accessPoint = parsedArn.resource.split(delimiter)[1];
+    var accessPointPrefix = accessPoint + '-' + parsedArn.accountId;
+    if (!s3util.dnsCompatibleBucketName(accessPointPrefix) || accessPointPrefix.match(/\./)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Access point resource in ARN is not DNS compatible. Got ' + accessPoint
+      });
+    }
+
+    //set parsed valid access point
+    req.service._parsedArn.accessPoint = accessPoint;
+  },
+
+  /**
+   * Validate Outposts ARN supplied in Bucket parameter is a valid outposts ARN
+   */
+  validateOutpostsArn: function validateOutpostsArn(req) {
+    var parsedArn = req.service._parsedArn;
+
+    if (
+      parsedArn.resource.indexOf('outpost:') !== 0 &&
+      parsedArn.resource.indexOf('outpost/') !== 0
+    ) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'ARN resource should begin with \'outpost/\''
+      });
+    }
+
+    //can be ':' or '/'
+    var delimiter = parsedArn.resource['outpost'.length];
+    var outpostId = parsedArn.resource.split(delimiter)[1];
+    var dnsHostRegex = new RegExp(/^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$/);
+    if (!dnsHostRegex.test(outpostId)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Outpost resource in ARN is not DNS compatible. Got ' + outpostId
+      });
+    }
+    req.service._parsedArn.outpostId = outpostId;
+  },
+
+  /**
+   * Validate Outposts ARN supplied in Bucket parameter is a valid outposts ARN
+   */
+  validateOutpostsAccessPointArn: function validateOutpostsAccessPointArn(req) {
+    var parsedArn = req.service._parsedArn;
+
+    //can be ':' or '/'
+    var delimiter = parsedArn.resource['outpost'.length];
+
+    if (parsedArn.resource.split(delimiter).length !== 4) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Outposts ARN should have two resources outpost/{outpostId}/accesspoint/{accesspointName}'
+      });
+    }
+
+    var accessPoint = parsedArn.resource.split(delimiter)[3];
+    var accessPointPrefix = accessPoint + '-' + parsedArn.accountId;
+    if (!s3util.dnsCompatibleBucketName(accessPointPrefix) || accessPointPrefix.match(/\./)) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'Access point resource in ARN is not DNS compatible. Got ' + accessPoint
+      });
+    }
+
+    //set parsed valid access point
+    req.service._parsedArn.accessPoint = accessPoint;
+  },
+
+  /**
+   * Validate region field in ARN supplied in Bucket parameter is a valid region
+   */
+  validateArnRegion: function validateArnRegion(req) {
+    var useArnRegion = s3util.loadUseArnRegionConfig(req);
+    var regionFromArn = req.service._parsedArn.region;
+    var clientRegion = req.service.config.region;
+
+    if (!regionFromArn) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidARN',
+        message: 'ARN region is empty'
+      });
+    }
+
+    if (
+      clientRegion.indexOf('fips') >= 0 ||
+      regionFromArn.indexOf('fips') >= 0
+    ) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'ARN 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'
+      });
+    }
+
+    if (req.service._parsedArn.service === 's3-outposts' && req.service.config.useDualstack) {
+      throw AWS.util.error(new Error(), {
+        code: 'InvalidConfiguration',
+        message: 'useDualstack config is not supported with outposts access point ARN'
+      });
+    }
+  },
+
+  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;
+  },
+
+  /**
+   * Validations before URI can be populated
+   */
+  validatePopulateUriFromArn: function validatePopulateUriFromArn(req) {
+    if (req.service._originalConfig && 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'
+      });
+    }
+  },
+
+  /**
+   * Returns true if the bucket name is DNS compatible.  Buckets created
+   * outside of the classic region MUST be DNS compatible.
+   *
+   * @api private
+   */
+  dnsCompatibleBucketName: function dnsCompatibleBucketName(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+/);
+    var dots = new RegExp(/\.\./);
+    return (b.match(domain) && !b.match(ipAddress) && !b.match(dots)) ? true : false;
+  },
+};
+
+/**
+ * @api private
+ */
+module.exports = s3util;
diff --git a/scripts/region-checker/allowlist.js b/scripts/region-checker/allowlist.js
index 263529e1f..a3680c3c2 100644
--- a/scripts/region-checker/allowlist.js
+++ b/scripts/region-checker/allowlist.js
@@ -31,19 +31,19 @@ var allowlist = {
         319
     ],
     '/services/s3.js': [
-        70,
-        71,
-        362,
-        364,
-        377,
-        383,
-        767,
-        769,
-        888,
-        899,
-        900,
-        901,
-        906
+        81,
+        82,
+        246,
+        248,
+        261,
+        267,
+        630,
+        632,
+        751,
+        762,
+        763,
+        764,
+        769
     ]
 };
 


Test patch:
diff --git a/test/event_listeners.spec.js b/test/event_listeners.spec.js
index 269ebaab2..25893df57 100644
--- a/test/event_listeners.spec.js
+++ b/test/event_listeners.spec.js
@@ -413,10 +413,10 @@
         response = request.send(function() {});
         return expect(response.error.message).to.equal('ERROR');
       });
-      it('uses the api.signingName if provided', function() {
+      return it('uses the signingName from service', function() {
         var request, response;
         helpers.mockHttpResponse(200, {}, '');
-        service.api.signingName = 'SIGNING_NAME';
+        service.getSigningName = function() { return 'SIGNING_NAME';};
         helpers.spyOn(AWS.Signers.RequestSigner, 'getVersion').andCallFake(function() {
           return function(req, signingName) {
             throw signingName;
@@ -427,18 +427,6 @@
         expect(response.error).to.equal('SIGNING_NAME');
         return delete service.api.signingName;
       });
-      return it('uses the api.endpointPrefix if signingName not provided', function() {
-        var request, response;
-        helpers.mockHttpResponse(200, {}, '');
-        helpers.spyOn(AWS.Signers.RequestSigner, 'getVersion').andCallFake(function() {
-          return function(req, signingName) {
-            throw signingName;
-          };
-        });
-        request = makeRequest();
-        response = request.send(function() {});
-        return expect(response.error).to.equal('mockservice');
-      });
     });
 
     describe('send', function() {
diff --git a/test/service.spec.js b/test/service.spec.js
index 59f631099..b1f083657 100644
--- a/test/service.spec.js
+++ b/test/service.spec.js
@@ -717,6 +717,24 @@
         });
       });
     });
+    describe('getServiceName', function() {
+      it('should return api.signingName if provided', function(done) {
+        service = new AWS.Lambda();
+        service.api.signingName = 'SIGNING_NAME';
+        expect(service.getSigningName()).to.equal(
+          service.api.signingName
+        );
+        done();
+      });
+      it('should return api.endpointPrefix if signingName is not provided', function(done) {
+        service = new AWS.Lambda();
+        service.api.endpointPrefix = 'SIGNING_NAME';
+        expect(service.getSigningName()).to.equal(
+          service.api.endpointPrefix
+        );
+        done();
+      });
+    });
     describe('getSignerClass', function() {
       var getVersion;
       getVersion = function(signer) {
diff --git a/test/services/s3.spec.js b/test/services/s3.spec.js
index 1dab7204a..f3c7708e0 100644
--- a/test/services/s3.spec.js
+++ b/test/services/s3.spec.js
@@ -21,45 +21,6 @@ describe('AWS.S3', function() {
     return done();
   });
 
-  describe('dnsCompatibleBucketName', function() {
-    it('must be at least 3 characters', function() {
-      expect(s3.dnsCompatibleBucketName('aa')).to.equal(false);
-    });
-
-    it('must not be longer than 63 characters', function() {
-      var b;
-      b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
-      expect(s3.dnsCompatibleBucketName(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);
-    });
-
-    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);
-    });
-
-    it('may not contain multiple contiguous dots', function() {
-      expect(s3.dnsCompatibleBucketName('abc.123')).to.equal(true);
-      expect(s3.dnsCompatibleBucketName('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);
-    });
-
-    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);
-    });
-  });
-
   describe('constructor', function() {
     it('requires endpoint if s3BucketEndpoint is passed', function() {
       expect(function() {
@@ -412,6 +373,31 @@ describe('AWS.S3', function() {
     });
   });
 
+  describe('getSigningName', function() {
+    it('returns serviceName from BucketArn if present', function(done) {
+      s3 = new AWS.S3({
+        region: 'us-east-1'
+      });
+      s3._parsedArn = { service: 'SERVICE_NAME' };
+      expect(s3.getSigningName()).to.equal(
+        s3._parsedArn.service
+      );
+      done();
+    });
+
+    it('returns serviceName from super.signingName if BucketArn not present', function(done) {
+      s3 = new AWS.S3({
+        region: 'us-east-1'
+      });
+      var serviceName = 'SERVICE_NAME';
+      var getSigningName = AWS.Service.prototype.getSigningName;
+      AWS.Service.prototype.getSigningName = function() { return serviceName; };
+      expect(s3.getSigningName()).to.equal(serviceName);
+      AWS.Service.prototype.getSigningName = getSigningName;
+      done();
+    });
+  });
+
   describe('getSignerClass', function() {
     var getVersion;
     getVersion = function(signer) {
@@ -3064,7 +3050,20 @@ describe('AWS.S3', function() {
         done();
       });
     });
+
+    it('supports outposts ARN', function(done) {
+      s3.getSignedUrl('getObject', {
+        Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myendpoint',
+        Key: 'key'
+      }, function(err, data) {
+        expect(data).to.equal(
+          'https://myendpoint-123456789012.op-01234567890123456.s3-outposts.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;
     err = null;
@@ -3477,237 +3476,344 @@ describe('AWS.S3', function() {
       }
     });
 
-    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();
+    describe('validateArnService', function() {
+      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('InvalidARN');
+          expect(err.message).to.equal('expect \'s3\' or \'s3-outposts\' in ARN service component');
+          done();
+        });
       });
     });
 
-    it('should correctly generate access point endpoint with no client config', function(done) {
-      helpers.mockHttpResponse(200, {}, '');
-      client = new AWS.S3();
-      var request = client.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();
+    describe('validateArnAccount', function() {
+      it('should throw if supplied empty accountId in ARN', function(done) {
+        s3 = new AWS.S3();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-west-2::accesspoint:mybucket',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN accountID does not match regex "[0-9]{12}"');
+          done();
+        });
       });
-    });
 
-    it('should correctly generate access point endpoint containing \':\'', 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 throw if supplied invalid accountId in ARN', function(done) {
+        s3 = new AWS.S3();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-west-2:1234567890:accesspoint:mybucket',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN accountID does not match regex "[0-9]{12}"');
+          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();
+    describe('validateArnRegion', function() {
+      it('should throw if supplied empty region in ARN', function(done) {
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3::123456789012:accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN region is empty');
+          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');
 
-    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'
+        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('ARN endpoint is not compatible with FIPS region');
       });
-      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;
+      it('should 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();
+        });
       });
-      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();
-      });
-    });
+      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 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 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();
+        });
       });
-    });
 
-    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'
+      it('should throw if useAccelerateEndpoint is set to true', 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();
+        });
       });
-      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 useDualstack it set to true for outposts Arn', function(done) {
+        s3 = new AWS.S3({region: 'us-west-2', useDualstack: true});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('useDualstack config is not supported with outposts 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();
+    describe('validateArnResourceType', function() {
+      it('should throw if access point ARN is not for access point resource', 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('InvalidARN');
+          expect(err.message).to.equal('ARN resource should begin with \'accesspoint/\'');
+          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'
+    describe('validateS3AccessPointArn', function() {
+      it('should throw if access point ARN resource format is incorrect', function(done) {
+        s3 = new AWS.S3();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3:us-west-2:123456789012:accesspoint:mybucket:blah',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('Access Point ARN should have one resource accesspoint/{accesspointName}');
+          done();
+        });
       });
-      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 bucket name 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('InvalidARN');
+          expect(err.message).to.equal('Access point resource in ARN is not DNS compatible. Got my.bucket');
+          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'
+    describe('validateOutpostsArn', function() {
+      it('should throw if ARN doesn\'t begin with outpost', function(done) {
+        s3 = new AWS.S3();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:bucket_name:mybucket',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN resource should begin with \'outpost/\'');
+          done();
+        });
       });
-      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();
+
+      it('should throw if outpost ID is not valid RFC 3986 Host', function(done) {
+        s3 = new AWS.S3();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op.outpostId:accesspoint:mybucket',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('Outpost resource in ARN is not DNS compatible. Got op.outpostId');
+          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'});
+    describe('validateOutpostsAccessPointArn', function() {
+      it('should throw if ARN resource format is incorrect', function(done) {
+        s3 = new AWS.S3();
         helpers.mockHttpResponse(200, {}, '');
         var request = s3.getObject({
-          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint/myendpoint',
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:mybucket:blah',
           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');
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal(
+            'Outposts ARN should have two resources outpost/{outpostId}/accesspoint/{accesspointName}'
+          );
           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'});
+      it('should throw if bucket name is not dns compatible', function(done) {
+        s3 = new AWS.S3();
         helpers.mockHttpResponse(200, {}, '');
         var request = s3.getObject({
-          Bucket: 'arn:aws:s3:us-east-1:123456789012:accesspoint/myendpoint',
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:my.bucket',
           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');
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('Access point resource in ARN is not DNS compatible. Got my.bucket');
           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'});
+    describe('validatePopulateUriFromArn', function() {
+      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',
@@ -3716,16 +3822,13 @@ describe('AWS.S3', function() {
         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');
+          expect(err.message).to.equal('Custom endpoint is not compatible with access point ARN');
           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'});
+      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',
@@ -3734,139 +3837,155 @@ describe('AWS.S3', function() {
         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');
+          expect(err.message).to.equal('Cannot construct path-style endpoint with access point');
           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();
-      });
-    });
+    [
+      {
+        service: 's3',
+        resource: '',
+        endpoint: 's3-accesspoint'
+      },
+      {
+        service: 's3-outposts',
+        resource: 'outpost/op-01234567890123456/',
+        endpoint: 'op-01234567890123456.s3-outposts'
+      }
+    ].forEach(function(input) {
 
-    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();
-      });
-    });
+      var service = input.service;
+      var resource = input.resource;
+      var endpoint = input.endpoint;
 
-    it('should throw if supplied empty accountId in ARN', function(done) {
-      s3 = new AWS.S3();
-      helpers.mockHttpResponse(200, {}, '');
-      var request = s3.getObject({
-        Bucket: 'arn:aws:s3:us-west-2::accesspoint: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 accountID does not match regex "[0-9]{12}"');
-        done();
+      it('should correctly generate access point endpoint', function(done) {
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:' + service + ':us-west-2:123456789012:' + resource + 'accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.not.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.us-west-2.amazonaws.com'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          done();
+        });
       });
-    });
 
-    it('should throw if supplied invalid accountId in ARN', function(done) {
-      s3 = new AWS.S3();
-      helpers.mockHttpResponse(200, {}, '');
-      var request = s3.getObject({
-        Bucket: 'arn:aws:s3:us-west-2:1234567890:accesspoint: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 accountID does not match regex "[0-9]{12}"');
-        done();
+      it('should correctly generate access point endpoint with no client config', function(done) {
+        helpers.mockHttpResponse(200, {}, '');
+        client = new AWS.S3();
+        var request = client.getObject({
+          Bucket: 'arn:aws:' + service + ':us-west-2:123456789012:' + resource + 'accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.not.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.us-west-2.amazonaws.com'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          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 correctly generate access point endpoint containing \':\'', function(done) {
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.getObject({
+          Bucket: 'arn:aws:' + service + ':us-west-2:123456789012:' + resource.split('/').join(':') + 'accesspoint:myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.not.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.us-west-2.amazonaws.com'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          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'
+      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:' + service + ':us-gov-east-1:123456789012:' + resource + 'accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.us-gov-east-1.amazonaws.com'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          done();
+        });
       });
-      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 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:' + service + ':cn-north-1:123456789012:' + resource + 'accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.not.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.cn-north-1.amazonaws.com.cn'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          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'
+      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:' + service + ':us-east-1:123456789012:' + resource + 'accesspoint/myendpoint',
+          Key: 'key'
+        });
+        request.send(function(err, data) {
+          expect(err).to.not.exist;
+          expect(request.httpRequest.endpoint.hostname).to.equal(
+            'myendpoint-123456789012.' + endpoint + '.us-east-1.amazonaws.com'
+          );
+          expect(request.httpRequest.path).to.equal('/key');
+          done();
+        });
       });
-      request.send(function(err, data) {
-        expect(err).to.exist;
-        expect(err.name).to.equal('InvalidAccessPointARN');
-        done();
+
+      it('should not apply access point ARN parsing to CreateBucketAPI', function(done) {
+        helpers.mockHttpResponse(200, {}, '');
+        var request = s3.createBucket({
+          Bucket: 'arn:aws:' + service + ':us-east-1:123456789012:' + resource.split('/').join(':') + '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%3A' + service + '%3Aus-east-1%3A123456789012%3A' + resource.split('/').join('%3A') + 'accesspoint%3Amyendpoint'
+          );
+          done();
+        });
       });
     });
 
-    it('should throw if access point ARN has extra resource component', function(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:mybucket:object:foo',
+        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('InvalidAccessPointARN');
+        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();
       });
     });
diff --git a/test/services/s3control.spec.js b/test/services/s3control.spec.js
index 8e4a549e0..0ed280907 100644
--- a/test/services/s3control.spec.js
+++ b/test/services/s3control.spec.js
@@ -2,48 +2,13 @@ var helpers = require('../helpers');
 var AWS = helpers.AWS;
 
 describe('AWS.S3Control', function() {
-  it('append accountId to hostname when supplied', function() {
-    var client = new AWS.S3Control({region: 'us-east-1'});
-    var request = client.getPublicAccessBlock({AccountId: '111'});
-    helpers.mockResponse({data: {}});
-    request.send();
-    expect(request.httpRequest.headers['x-amz-account-id']).to.eql(undefined);
-    expect(request.httpRequest.headers.Host).to.eql('111.s3-control.us-east-1.amazonaws.com');
-    expect(request.httpRequest.endpoint.hostname).to.eql('111.s3-control.us-east-1.amazonaws.com');
-  });
 
-  it('shouldn\'t append accountId if not supplied', function() {
-    var client = new AWS.S3Control({region: 'us-east-1'});
-    var request = client.putPublicAccessBlock({});
-    helpers.mockResponse({data: {}});
-    request.send();
-    expect(request.httpRequest.headers.Host).to.eql('s3-control.us-east-1.amazonaws.com');
-    expect(request.httpRequest.endpoint.hostname).to.eql('s3-control.us-east-1.amazonaws.com');
+  var env = process.env;
+  afterEach(function() {
+    process.env = env;
   });
-
-  it('append accountId to hostname when supplied and using dualstack', function() {
-    var client = new AWS.S3Control({region: 'us-east-1', useDualstack: true});
-    var request = client.getPublicAccessBlock({AccountId: '222'});
-    helpers.mockResponse({data: {}});
-    request.send();
-    expect(request.httpRequest.headers.Host).to.eql('222.s3-control.dualstack.us-east-1.amazonaws.com');
-    expect(request.httpRequest.endpoint.hostname).to.eql('222.s3-control.dualstack.us-east-1.amazonaws.com');
-
-    client = new AWS.S3Control({region: 'cn-north-1', useDualstack: true});
-    request = client.getPublicAccessBlock({AccountId: '222'});
-    helpers.mockResponse({data: {}});
-    request.send();
-    expect(request.httpRequest.headers.Host).to.eql('222.s3-control.dualstack.cn-north-1.amazonaws.com.cn');
-    expect(request.httpRequest.endpoint.hostname).to.eql('222.s3-control.dualstack.cn-north-1.amazonaws.com.cn');
-  });
-
-  it('append accountId to hostname when supplied and using customized endpoint', function() {
-    var client = new AWS.S3Control({endpoint: 's3-control-fips.us-east-1.amazonaws.com'});
-    var request = client.getPublicAccessBlock({AccountId: '222'});
-    helpers.mockResponse({data: {}});
-    request.send();
-    expect(request.httpRequest.headers.Host).to.eql('222.s3-control-fips.us-east-1.amazonaws.com');
-    expect(request.httpRequest.endpoint.hostname).to.eql('222.s3-control-fips.us-east-1.amazonaws.com');
+  beforeEach(function() {
+    process.env = {};
   });
 
   it('should add hostId and requestId to exception response', function() {
@@ -134,8 +99,6 @@ describe('AWS.S3Control', function() {
       var response = request.response;
       expect(response.error).to.be.null;
     }
-
-
   });
 
   it('validate accountId pattern', function() {
@@ -157,4 +120,359 @@ describe('AWS.S3Control', function() {
       expect(request.response.error).to.be.null;
     }
   });
+
+  describe('Access Point', function() {
+    var iniLoader = AWS.util.iniLoader;
+    afterEach(function() {
+      if (AWS.util.isNode()) {
+        iniLoader.clearCachedFiles();
+      }
+    });
+
+    describe('validateArnService', function() {
+      it('should throw if supplied non-s3 ARN', function(done) {
+        var client = new AWS.S3Control();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:sqs:us-west-2:123456789012:someresource'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('expect \'s3\' or \'s3-outposts\' in ARN service component');
+          done();
+        });
+      });
+    });
+
+    describe('validateArnAccount', function() {
+      it('should throw if supplied empty accountId in ARN', function(done) {
+        var client = new AWS.S3Control();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-west-2::outpost:op-01234567890123456:bucket:mybucket'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN accountID does not match regex "[0-9]{12}"');
+          done();
+        });
+      });
+
+      it('should throw if supplied invalid accountId in ARN', function(done) {
+        var client = new AWS.S3Control();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:invalidaccountID:outpost:op-01234567890123456:bucket:mybucket'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN accountID does not match regex "[0-9]{12}"');
+          done();
+        });
+      });
+
+      it('should throw if different accountId is passed in ARN and in params', function(done) {
+        var client = new AWS.S3Control();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket',
+          AccountId: '123456789021'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('ValidationError');
+          expect(err.message).to.equal('AccountId in ARN and request params should be same.');
+          done();
+        });
+      });
+    });
+
+    describe('validateArnRegion', function() {
+      it('should throw if supplied empty region in ARN', function(done) {
+        var client = new AWS.S3Control();
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts::123456789012:bucket:mybucket'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidARN');
+          expect(err.message).to.equal('ARN region is empty');
+          done();
+        });
+      });
+
+      it('should correctly generate access point endpoint for pseudo regions', function() {
+        var client = new AWS.S3Control({region: 'us-east-1'});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:s3-external-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+        });
+        var built = request.build(function() {});
+        expect(
+          built.httpRequest.endpoint.hostname
+        ).to.equal('s3-outposts.s3-external-1.amazonaws.com');
+
+        client = new AWS.S3Control({region: 'us-east-1-fips'});
+        helpers.mockHttpResponse(200, {}, '');
+        request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:s3-external-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+        });
+        var error;
+        request.build(function(err) {
+          error = err;
+        });
+        expect(error.name).to.equal('InvalidConfiguration');
+        expect(error.message).to.equal('ARN endpoint is not compatible with FIPS region');
+      });
+
+      it('should use regions from ARN if s3UseArnRegion config is set to false', function(done) {
+        var client = new AWS.S3Control({region: 'us-west-2', s3UseArnRegion: false});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+        });
+        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();
+        });
+      });
+
+      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';
+          var client = new AWS.S3Control({region: 'us-west-2'});
+          helpers.mockHttpResponse(200, {}, '');
+          var request = client.getBucket({
+            Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+          });
+          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';
+          var client = new AWS.S3Control({region: 'us-west-2'});
+          helpers.mockHttpResponse(200, {}, '');
+          var request = client.getBucket({
+            Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+          });
+          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);
+          var client = new AWS.S3Control({region: 'us-west-2'});
+          helpers.mockHttpResponse(200, {}, '');
+          var request = client.getBucket({
+            Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+          });
+          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);
+          var client = new AWS.S3Control({region: 'us-west-2'});
+          helpers.mockHttpResponse(200, {}, '');
+          var request = client.getBucket({
+            Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+          });
+          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 cross region enabled but region from different partition', function(done) {
+        var client = new AWS.S3Control({region: 'us-west-2'});
+        var request = client.getBucket({
+          Bucket: 'arn:aws-cn:s3:cn-north-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+        });
+        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();
+        });
+      });
+
+      it('should throw if useAccelerateEndpoint is set to true', function(done) {
+        var client = new AWS.S3Control({region: 'us-west-2', useAccelerateEndpoint: true});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+        });
+        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 throw if useDualstack it set to true for outposts Arn', function(done) {
+        var client = new AWS.S3Control({region: 'us-west-2', useDualstack: true});
+        helpers.mockHttpResponse(200, {}, '');
+        var request = client.getBucket({
+          Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost/op-01234567890123456/accesspoint/myendpoint'
+        });
+        request.send(function(err, data) {
+          expect(err).to.exist;
+          expect(err.name).to.equal('InvalidConfiguration');
+          expect(err.message).to.equal('useDualstack config is not supported with outposts access point ARN');
+          done();
+        });
+      });
+    });
+  });
+
+  describe('validateOutpostsArn', function() {
+    it('should throw if ARN doesn\'t begin with outpost', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:bucket_name:mybucket'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal('ARN resource should begin with \'outpost/\'');
+        done();
+      });
+    });
+
+    it('should throw if outpost ID is not valid RFC 3986 Host', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op.outpostId:accesspoint:mybucket'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal('Outpost resource in ARN is not DNS compatible. Got op.outpostId');
+        done();
+      });
+    });
+  });
+
+  describe('validateOutpostsAccessPointArn', function() {
+    it('should throw if ARN resource format is incorrect', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getAccessPoint({
+        Name: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:mybucket:blah'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal(
+          'Outposts ARN should have two resources outpost/{outpostId}/accesspoint/{accesspointName}'
+        );
+        done();
+      });
+    });
+
+    it('should throw if name is not dns compatible', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getAccessPoint({
+        Name: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:my.accesspoint'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal('Access point resource in ARN is not DNS compatible. Got my.accesspoint');
+        done();
+      });
+    });
+  });
+
+  describe('validateOutpostsBucketArn', function() {
+    it('should throw if ARN resource format is incorrect', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:mybucket:blah'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal(
+          'Bucket ARN should have two resources outpost/{outpostId}/bucket/{accesspointName}'
+        );
+        done();
+      });
+    });
+
+    it('should throw if name is not dns compatible', function(done) {
+      var client = new AWS.S3Control();
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:my.bucket'
+      });
+      request.send(function(err, data) {
+        expect(err).to.exist;
+        expect(err.name).to.equal('InvalidARN');
+        expect(err.message).to.equal('Bucket ARN is not DNS compatible. Got my.bucket');
+        done();
+      });
+    });
+  });
+
+  describe('validatePopulateUriFromArn', function() {
+    it('should throw if manually-set endpoint is present', function(done) {
+      var client = new AWS.S3Control({region: 'us-west-2', endpoint: 'https://bucket.s3.us-west-2.amazonaws.com'});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+      });
+      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) {
+      var client = new AWS.S3Control({s3ForcePathStyle: true});
+      helpers.mockHttpResponse(200, {}, '');
+      var request = client.getBucket({
+        Bucket: 'arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-01234567890123456/bucket/mybucket'
+      });
+      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();
+      });
+    });
+  });
 });
diff --git a/test/services/s3util.spec.js b/test/services/s3util.spec.js
new file mode 100644
index 000000000..369281e8d
--- /dev/null
+++ b/test/services/s3util.spec.js
@@ -0,0 +1,40 @@
+var helpers = require('../helpers');
+var s3util = require('../../lib/services/s3util');
+
+describe('dnsCompatibleBucketName', function() {
+  it('must be at least 3 characters', function() {
+    expect(s3util.dnsCompatibleBucketName('aa')).to.equal(false);
+  });
+
+  it('must not be longer than 63 characters', function() {
+    var b;
+    b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+    expect(s3util.dnsCompatibleBucketName(b)).to.equal(false);
+  });
+
+  it('must start with a lower-cased letter or number', function() {
+    expect(s3util.dnsCompatibleBucketName('Abc')).to.equal(false);
+    expect(s3util.dnsCompatibleBucketName('-bc')).to.equal(false);
+  });
+
+  it('must end with a lower-cased letter or number', function() {
+    expect(s3util.dnsCompatibleBucketName('abC')).to.equal(false);
+    expect(s3util.dnsCompatibleBucketName('ab-')).to.equal(false);
+    expect(s3util.dnsCompatibleBucketName('abc')).to.equal(true);
+  });
+
+  it('may not contain multiple contiguous dots', function() {
+    expect(s3util.dnsCompatibleBucketName('abc.123')).to.equal(true);
+    expect(s3util.dnsCompatibleBucketName('abc..123')).to.equal(false);
+  });
+
+  it('may only contain letters numbers and dots', function() {
+    expect(s3util.dnsCompatibleBucketName('abc123')).to.equal(true);
+    expect(s3util.dnsCompatibleBucketName('abc_123')).to.equal(false);
+  });
+
+  it('must not look like an ip address', function() {
+    expect(s3util.dnsCompatibleBucketName('1.2.3.4')).to.equal(false);
+    expect(s3util.dnsCompatibleBucketName('a.b.c.d')).to.equal(true);
+  });
+});