diff --git a/backend/logic/deploy/bucket/aws.js b/backend/logic/deploy/bucket/aws.js index d6a125b45..569438698 100644 --- a/backend/logic/deploy/bucket/aws.js +++ b/backend/logic/deploy/bucket/aws.js @@ -7,7 +7,11 @@ const { isConfigured } = require('@origin/dshop-validation/matrix') const { guessContentType, walkDir } = require('../../../utils/filesystem') const { assert } = require('../../../utils/validators') const { BucketExistence } = require('../../../utils/enums') -const { NETWORK_ID_TO_NAME, BUCKET_PREFIX } = require('../../../utils/const') +const { + NETWORK_ID_TO_NAME, + AWS_MARKETPLACE_DEPLOYMENT, + BUCKET_PREFIX +} = require('../../../utils/const') const { getLogger } = require('../../../utils/logger') const log = getLogger('logic.deploy.bucket.aws') @@ -24,7 +28,7 @@ let cachedClient = null function isAvailable({ networkConfig, resourceSelection }) { return ( resourceSelection.includes('aws-files') && - isConfigured(networkConfig, 'aws-files') + (AWS_MARKETPLACE_DEPLOYMENT || isConfigured(networkConfig, 'aws-files')) ) } @@ -34,13 +38,23 @@ function isAvailable({ networkConfig, resourceSelection }) { * @param args {Object} * @param args.networkConfig {Object} - Decrypted networkConfig object */ + function configure({ networkConfig }) { - cachedClient = new S3({ - apiVersion: '2006-03-01', - accessKeyId: networkConfig.awsAccessKeyId, - secretAccessKey: networkConfig.awsSecretAccessKey, - region: networkConfig.awsRegion // Optional - }) + if (AWS_MARKETPLACE_DEPLOYMENT) { + //Use the credentials from the EC2 Instance metadata + cachedClient = new S3({ + apiVersion: '2006-03-01', + region: networkConfig.awsRegion // Optional + }) + } else { + //Look up the shop admin's AWS credentials from the network config + cachedClient = new S3({ + apiVersion: '2006-03-01', + accessKeyId: networkConfig.awsAccessKeyId, + secretAccessKey: networkConfig.awsSecretAccessKey, + region: networkConfig.awsRegion // Optional + }) + } } /** @@ -81,7 +95,11 @@ async function deploy({ shop, networkConfig, OutputDir }) { } } - await cachedClient.createBucket(params).promise() + await cachedClient + .createBucket(params, (err) => { + console.error(`Cannot create S3 Bucket:\n`, err) + }) + .promise() log.debug('Waiting for bucket to exist...') diff --git a/backend/logic/deploy/index.js b/backend/logic/deploy/index.js index 12b791e32..cbacfdfd5 100644 --- a/backend/logic/deploy/index.js +++ b/backend/logic/deploy/index.js @@ -33,7 +33,11 @@ const { getLogger } = require('../../utils/logger') const { getMyIP } = require('../../utils/ip') const { assert } = require('../../utils/validators') const { hasCNAMEOrA } = require('../../utils/dns') -const { DSHOP_CACHE, DEFAULT_INFRA_RESOURCES } = require('../../utils/const') +const { + AWS_MARKETPLACE_DEPLOYMENT, + DSHOP_CACHE, + DEFAULT_INFRA_RESOURCES +} = require('../../utils/const') const { deploymentLock, @@ -309,6 +313,7 @@ async function deploy({ selection: resourceSelection, id: 'gcp-files' }) || + AWS_MARKETPLACE_DEPLOYMENT || canUseResource({ networkConfig, selection: resourceSelection, @@ -323,6 +328,7 @@ async function deploy({ dataDir, resourceSelection }) + log.debug(`Result of bucket deployment: #${responses}`) if (responses.length > 0) { bucketUrls = responses.map((r) => r.url) bucketHttpUrls = responses.map((r) => r.httpUrl) diff --git a/backend/scripts/aws/marketplace/AWS_CloudFormation_Template.json b/backend/scripts/aws/marketplace/AWS_CloudFormation_Template.json new file mode 100644 index 000000000..b839f1aee --- /dev/null +++ b/backend/scripts/aws/marketplace/AWS_CloudFormation_Template.json @@ -0,0 +1,319 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + + "Description": "AWS CloudFormation Sample Template Create an Amazon EC2 instance running the DShop AMI. This example creates an EC2 security group for the instance to give you SSH access. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.", + + "Parameters": { + "KeyName": { + "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance", + "Type": "AWS::EC2::KeyPair::KeyName", + "ConstraintDescription": "must be the name of an existing EC2 KeyPair." + }, + + "InstanceType": { + "Description": "WebServer EC2 instance type", + "Type": "String", + "Default": "t2.small", + "AllowedValues": [ + "t1.micro", + "t2.nano", + "t2.micro", + "t2.small", + "t2.medium", + "t2.large", + "m1.small", + "m1.medium", + "m1.large", + "m1.xlarge", + "m2.xlarge", + "m2.2xlarge", + "m2.4xlarge", + "m3.medium", + "m3.large", + "m3.xlarge", + "m3.2xlarge", + "m4.large", + "m4.xlarge", + "m4.2xlarge", + "m4.4xlarge", + "m4.10xlarge", + "c1.medium", + "c1.xlarge", + "c3.large", + "c3.xlarge", + "c3.2xlarge", + "c3.4xlarge", + "c3.8xlarge", + "c4.large", + "c4.xlarge", + "c4.2xlarge", + "c4.4xlarge", + "c4.8xlarge", + "g2.2xlarge", + "g2.8xlarge", + "r3.large", + "r3.xlarge", + "r3.2xlarge", + "r3.4xlarge", + "r3.8xlarge", + "i2.xlarge", + "i2.2xlarge", + "i2.4xlarge", + "i2.8xlarge", + "d2.xlarge", + "d2.2xlarge", + "d2.4xlarge", + "d2.8xlarge", + "hi1.4xlarge", + "hs1.8xlarge", + "cr1.8xlarge", + "cc2.8xlarge", + "cg1.4xlarge" + ], + "ConstraintDescription": "must be a valid EC2 instance type." + }, + + "SSHLocation": { + "Description": "The IP address range that can be used to SSH to the EC2 instances", + "Type": "String", + "MinLength": "9", + "MaxLength": "18", + "Default": "0.0.0.0/0", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." + }, + + "DShopAdminAccessLocation": { + "Description": "The IP address range that can be used to access the DShop admin console", + "Type": "String", + "MinLength": "9", + "MaxLength": "18", + "Default": "0.0.0.0/0", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." + }, + + "AMIIdentifier": { + "Description": "The AMI Id to launch the EC2 instance from, if not the default ", + "Type": "String", + "Default": "ami-083b7cfcc9a1dc0cf" + } + }, + + "Mappings": { + "AWSInstanceType2Arch": { + "t1.micro": { "Arch": "HVM64" }, + "t2.nano": { "Arch": "HVM64" }, + "t2.micro": { "Arch": "HVM64" }, + "t2.small": { "Arch": "HVM64" }, + "t2.medium": { "Arch": "HVM64" }, + "t2.large": { "Arch": "HVM64" }, + "m1.small": { "Arch": "HVM64" }, + "m1.medium": { "Arch": "HVM64" }, + "m1.large": { "Arch": "HVM64" }, + "m1.xlarge": { "Arch": "HVM64" }, + "m2.xlarge": { "Arch": "HVM64" }, + "m2.2xlarge": { "Arch": "HVM64" }, + "m2.4xlarge": { "Arch": "HVM64" }, + "m3.medium": { "Arch": "HVM64" }, + "m3.large": { "Arch": "HVM64" }, + "m3.xlarge": { "Arch": "HVM64" }, + "m3.2xlarge": { "Arch": "HVM64" }, + "m4.large": { "Arch": "HVM64" }, + "m4.xlarge": { "Arch": "HVM64" }, + "m4.2xlarge": { "Arch": "HVM64" }, + "m4.4xlarge": { "Arch": "HVM64" }, + "m4.10xlarge": { "Arch": "HVM64" }, + "c1.medium": { "Arch": "HVM64" }, + "c1.xlarge": { "Arch": "HVM64" }, + "c3.large": { "Arch": "HVM64" }, + "c3.xlarge": { "Arch": "HVM64" }, + "c3.2xlarge": { "Arch": "HVM64" }, + "c3.4xlarge": { "Arch": "HVM64" }, + "c3.8xlarge": { "Arch": "HVM64" }, + "c4.large": { "Arch": "HVM64" }, + "c4.xlarge": { "Arch": "HVM64" }, + "c4.2xlarge": { "Arch": "HVM64" }, + "c4.4xlarge": { "Arch": "HVM64" }, + "c4.8xlarge": { "Arch": "HVM64" }, + "g2.2xlarge": { "Arch": "HVMG2" }, + "g2.8xlarge": { "Arch": "HVMG2" }, + "r3.large": { "Arch": "HVM64" }, + "r3.xlarge": { "Arch": "HVM64" }, + "r3.2xlarge": { "Arch": "HVM64" }, + "r3.4xlarge": { "Arch": "HVM64" }, + "r3.8xlarge": { "Arch": "HVM64" }, + "i2.xlarge": { "Arch": "HVM64" }, + "i2.2xlarge": { "Arch": "HVM64" }, + "i2.4xlarge": { "Arch": "HVM64" }, + "i2.8xlarge": { "Arch": "HVM64" }, + "d2.xlarge": { "Arch": "HVM64" }, + "d2.2xlarge": { "Arch": "HVM64" }, + "d2.4xlarge": { "Arch": "HVM64" }, + "d2.8xlarge": { "Arch": "HVM64" }, + "hi1.4xlarge": { "Arch": "HVM64" }, + "hs1.8xlarge": { "Arch": "HVM64" }, + "cr1.8xlarge": { "Arch": "HVM64" }, + "cc2.8xlarge": { "Arch": "HVM64" } + }, + + "AWSInstanceType2NATArch": { + "t1.micro": { "Arch": "NATHVM64" }, + "t2.nano": { "Arch": "NATHVM64" }, + "t2.micro": { "Arch": "NATHVM64" }, + "t2.small": { "Arch": "NATHVM64" }, + "t2.medium": { "Arch": "NATHVM64" }, + "t2.large": { "Arch": "NATHVM64" }, + "m1.small": { "Arch": "NATHVM64" }, + "m1.medium": { "Arch": "NATHVM64" }, + "m1.large": { "Arch": "NATHVM64" }, + "m1.xlarge": { "Arch": "NATHVM64" }, + "m2.xlarge": { "Arch": "NATHVM64" }, + "m2.2xlarge": { "Arch": "NATHVM64" }, + "m2.4xlarge": { "Arch": "NATHVM64" }, + "m3.medium": { "Arch": "NATHVM64" }, + "m3.large": { "Arch": "NATHVM64" }, + "m3.xlarge": { "Arch": "NATHVM64" }, + "m3.2xlarge": { "Arch": "NATHVM64" }, + "m4.large": { "Arch": "NATHVM64" }, + "m4.xlarge": { "Arch": "NATHVM64" }, + "m4.2xlarge": { "Arch": "NATHVM64" }, + "m4.4xlarge": { "Arch": "NATHVM64" }, + "m4.10xlarge": { "Arch": "NATHVM64" }, + "c1.medium": { "Arch": "NATHVM64" }, + "c1.xlarge": { "Arch": "NATHVM64" }, + "c3.large": { "Arch": "NATHVM64" }, + "c3.xlarge": { "Arch": "NATHVM64" }, + "c3.2xlarge": { "Arch": "NATHVM64" }, + "c3.4xlarge": { "Arch": "NATHVM64" }, + "c3.8xlarge": { "Arch": "NATHVM64" }, + "c4.large": { "Arch": "NATHVM64" }, + "c4.xlarge": { "Arch": "NATHVM64" }, + "c4.2xlarge": { "Arch": "NATHVM64" }, + "c4.4xlarge": { "Arch": "NATHVM64" }, + "c4.8xlarge": { "Arch": "NATHVM64" }, + "g2.2xlarge": { "Arch": "NATHVMG2" }, + "g2.8xlarge": { "Arch": "NATHVMG2" }, + "r3.large": { "Arch": "NATHVM64" }, + "r3.xlarge": { "Arch": "NATHVM64" }, + "r3.2xlarge": { "Arch": "NATHVM64" }, + "r3.4xlarge": { "Arch": "NATHVM64" }, + "r3.8xlarge": { "Arch": "NATHVM64" }, + "i2.xlarge": { "Arch": "NATHVM64" }, + "i2.2xlarge": { "Arch": "NATHVM64" }, + "i2.4xlarge": { "Arch": "NATHVM64" }, + "i2.8xlarge": { "Arch": "NATHVM64" }, + "d2.xlarge": { "Arch": "NATHVM64" }, + "d2.2xlarge": { "Arch": "NATHVM64" }, + "d2.4xlarge": { "Arch": "NATHVM64" }, + "d2.8xlarge": { "Arch": "NATHVM64" }, + "hi1.4xlarge": { "Arch": "NATHVM64" }, + "hs1.8xlarge": { "Arch": "NATHVM64" }, + "cr1.8xlarge": { "Arch": "NATHVM64" }, + "cc2.8xlarge": { "Arch": "NATHVM64" } + } + }, + + "Resources": { + "EC2Instance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "InstanceType": { "Ref": "InstanceType" }, + "SecurityGroups": [{ "Ref": "InstanceSecurityGroup" }], + "KeyName": { "Ref": "KeyName" }, + "ImageId": { "Ref": "AMIIdentifier" }, + "IamInstanceProfile": { "Ref": "IAMInstanceProfile"} + } + }, + + "InstanceSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Enable SSH access via port 22, and backend admin-console access via port 80 and 443", + "SecurityGroupIngress": [ + { + "IpProtocol": "tcp", + "FromPort": "22", + "ToPort": "22", + "CidrIp": { "Ref": "SSHLocation" } + }, + { + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "80", + "CidrIp": { "Ref": "DShopAdminAccessLocation" } + }, + { + "IpProtocol": "tcp", + "FromPort": "443", + "ToPort": "443", + "CidrIp": { "Ref": "DShopAdminAccessLocation" } + }, + { + "IpProtocol": "tcp", + "FromPort": "3000", + "ToPort": "3000", + "CidrIp": { "Ref": "DShopAdminAccessLocation" } + } + ] + } + }, + + "IAMrole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": ["ec2.amazonaws.com"] + }, + "Action": "sts:AssumeRole", + "Condition": {} + } + ] + }, + "Description": "IAM Role to allow integration between DShop and the following AWS services: Amazon S3, AWS CloudFront, AWS SES, Amazon Route 53", + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/AmazonS3FullAccess", + "arn:aws:iam::aws:policy/CloudFrontFullAccess", + "arn:aws:iam::aws:policy/AmazonSESFullAccess", + "arn:aws:iam::aws:policy/AmazonRoute53FullAccess" + ] + } + }, + + "IAMInstanceProfile": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "IAMrole" + } + ] + } + } + }, + + "Outputs": { + "InstanceId": { + "Description": "InstanceId of the newly created EC2 instance", + "Value": { "Ref": "EC2Instance" } + }, + "AZ": { + "Description": "Availability Zone of the newly created EC2 instance", + "Value": { "Fn::GetAtt": ["EC2Instance", "AvailabilityZone"] } + }, + "PublicDNS": { + "Description": "Public DNSName of the newly created EC2 instance", + "Value": { "Fn::GetAtt": ["EC2Instance", "PublicDnsName"] } + }, + "PublicIP": { + "Description": "Public IP address of the newly created EC2 instance", + "Value": { "Fn::GetAtt": ["EC2Instance", "PublicIp"] } + } + } +} diff --git a/backend/utils/const.js b/backend/utils/const.js index 69d3f28ae..b7954a43a 100644 --- a/backend/utils/const.js +++ b/backend/utils/const.js @@ -68,7 +68,8 @@ const { IPFS_GATEWAY, // IPFS gateway override BUCKET_PREFIX = DEFAULT_BUCKET_PREFIX, SERVICE_PREFIX = DEFAULT_SERVICE_PREFIX, - EXTERNAL_IP + EXTERNAL_IP, + AWS_MARKETPLACE_DEPLOYMENT //bool indicating whether the app is running from an AWS EC2 instance launched via the marketplace } = process.env /** @@ -128,6 +129,7 @@ module.exports = { BUCKET_PREFIX, SERVICE_PREFIX, EXTERNAL_IP, + AWS_MARKETPLACE_DEPLOYMENT, EXTERNAL_IP_SERVICE_URL, DEFAULT_INFRA_RESOURCES, DEFAULT_AWS_REGION, diff --git a/docs/dshop/deployment/aws-iam-role.md b/docs/dshop/deployment/aws-iam-role.md new file mode 100644 index 000000000..5039021ce --- /dev/null +++ b/docs/dshop/deployment/aws-iam-role.md @@ -0,0 +1,26 @@ +## Steps to create an AWS IAM role + +On a web browser, navigate to the 'Roles' tab in the IAM Dashboard of the AWS Management Console: https://console.aws.amazon.com/iamv2/home#/roles + +Click on the 'Create role' button +![IAM Dashboard](images/aws-iam-create-role-button.png) + +**NOTE:** If you do not see the 'Create role' button, log out of the console and log back in as a root user. + +Select 'Another AWS account' as the _Trusted Entity_ and enter your root user Account ID. You can find your Account ID by clicking on your profile on the top-right of the page: +![Enter details of Trusted Entity](images/aws-iam-enter-account-id.png) + +The policies you choose on the next screen will determine the kinds of (AWS) resources your DShop can access. For example, the policy **AmazonS3FullAccess** will allow DShop to create and delete Amazon S3 buckets on your behalf. Search for the following keywords to find other supported services: + - 'ses' (Amazon SES) + - 'cloudfront' (AWS CloudFront) + - 'route53' (Amazon Route 53) + +![Search for policies](images/aws-iam-search-for-s3.png) + +On the last screen, enter a name for the role and review your policy selections. Click on the 'Create role' button: + +![Review](images/aws-iam-enter-role-name.png) + +After you're redirected to the Roles page, click on the newly-created role and copy the Role ARN. This information will be used to allow DShop to interface with AWS by "assuming" the role. + +![Role ARN](images/aws-iam-role-arn.png) diff --git a/docs/dshop/deployment/images/aws-iam-create-role-button.png b/docs/dshop/deployment/images/aws-iam-create-role-button.png new file mode 100644 index 000000000..5aeaf234f Binary files /dev/null and b/docs/dshop/deployment/images/aws-iam-create-role-button.png differ diff --git a/docs/dshop/deployment/images/aws-iam-enter-account-id.png b/docs/dshop/deployment/images/aws-iam-enter-account-id.png new file mode 100644 index 000000000..6841e2359 Binary files /dev/null and b/docs/dshop/deployment/images/aws-iam-enter-account-id.png differ diff --git a/docs/dshop/deployment/images/aws-iam-enter-role-name.png b/docs/dshop/deployment/images/aws-iam-enter-role-name.png new file mode 100644 index 000000000..318232536 Binary files /dev/null and b/docs/dshop/deployment/images/aws-iam-enter-role-name.png differ diff --git a/docs/dshop/deployment/images/aws-iam-role-arn.png b/docs/dshop/deployment/images/aws-iam-role-arn.png new file mode 100644 index 000000000..7810a71d8 Binary files /dev/null and b/docs/dshop/deployment/images/aws-iam-role-arn.png differ diff --git a/docs/dshop/deployment/images/aws-iam-search-for-s3.png b/docs/dshop/deployment/images/aws-iam-search-for-s3.png new file mode 100644 index 000000000..9e04db092 Binary files /dev/null and b/docs/dshop/deployment/images/aws-iam-search-for-s3.png differ diff --git a/shop/package.json b/shop/package.json index 6681fbf48..e90cd415c 100644 --- a/shop/package.json +++ b/shop/package.json @@ -147,7 +147,7 @@ "ipfs-deploy": "8.0.1", "mini-css-extract-plugin": "1.3.6", "mocha": "8.2.1", - "node-sass": "5.0.0", + "node-sass": "6.0.0", "react-router-dom": "5.2.0", "sass-loader": "11.0.0", "style-loader": "2.0.0", diff --git a/shop/src/pages/super-admin/networks/_Form.js b/shop/src/pages/super-admin/networks/_Form.js index c1310f532..0c785329e 100644 --- a/shop/src/pages/super-admin/networks/_Form.js +++ b/shop/src/pages/super-admin/networks/_Form.js @@ -198,6 +198,32 @@ const NetworkForm = ({ onSave, network, feedback, className }) => { shopConfig: state.fallbackShopConfig }) + /* + * The Networks form should ask the shop admin for their AWS Credentials only when the DShop DApp is not deployed on an EC2 instance launched via AWS Marketplace. + * Reason: When DShop is deployed with the help of the Marketplace solution, the shop admin's AWS credentials can be obtained programatically. + * Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials + */ + const conditionallyRequestAWSCreds = () => { + if (process.env.AWS_MARKETPLACE_DEPLOYMENT) { + return + } else { + return ( +
+
+ + + {Feedback('awsAccessKeyId')} +
+
+ + + {Feedback('awsSecretAccessKey')} +
+
+ ) + } + } + const ProcessorIdToEmailComp = { sendgrid: SendgridModal, aws: AWSModal, @@ -408,18 +434,7 @@ const NetworkForm = ({ onSave, network, feedback, className }) => { {Feedback('gcpCredentials')} -
-
- - - {Feedback('awsAccessKeyId')} -
-
- - - {Feedback('awsSecretAccessKey')} -
-
+ {conditionallyRequestAWSCreds()}
diff --git a/yarn.lock b/yarn.lock index a65bf67e7..3bba0edc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18191,10 +18191,10 @@ node-releases@^1.1.70: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== -node-sass@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-5.0.0.tgz#4e8f39fbef3bac8d2dc72ebe3b539711883a78d2" - integrity sha512-opNgmlu83ZCF792U281Ry7tak9IbVC+AKnXGovcQ8LG8wFaJv6cLnRlc6DIHlmNxWEexB5bZxi9SZ9JyUuOYjw== +node-sass@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.0.tgz#f30da3e858ad47bfd138bc0e0c6f924ed2f734af" + integrity sha512-GDzDmNgWNc9GNzTcSLTi6DU6mzSPupVJoStIi7cF3GjwSE9q1cVakbvAAVSt59vzUjV9JJoSZFKoo9krbjKd2g== dependencies: async-foreach "^0.1.3" chalk "^1.1.1"