Success! WorkSpace removal in-progress... Please allow up to 10 minutes for the virtual desktop to be fully removed.');
177 | $("#methodStatus").show();
178 | setTimeout(function () {
179 | location.reload();
180 | }, 60000);
181 | }
182 | });
183 | }
184 |
185 | // The determineWorkspace function is called to get details of the assigned WorkSpace, and populate the WorkSpace details and action panes. In the event
186 | // there is no WorkSpace found, populate the list of bundles available and allow the user to request one. The function takes an optional eventSource
187 | // parameter to handle recursion if necessary to avoid an initial bundle listing edge case.
188 | function determineWorkspace(eventSource) {
189 | $.ajax({
190 | method: 'POST',
191 | url: WORKSPACES_CONTROL_URL,
192 | headers: {
193 | Authorization: authToken
194 | },
195 | beforeSend: function () {
196 | $("#loadDiv").show(); // Show a spinning loader to let the user know something is happening.
197 | },
198 | complete: function () {
199 | $("#loadDiv").hide(); // Hide the spinning loader once the AJAX call is complete.
200 | },
201 | data: JSON.stringify({
202 | action: 'list'
203 | }),
204 | contentType: 'text/plain',
205 | error: function () {
206 | $.ajax({
207 | method: 'POST',
208 | url: WORKSPACES_CONTROL_URL,
209 | headers: {
210 | Authorization: authToken
211 | },
212 | beforeSend: function () {
213 | $("#loadDiv").show(); // Show a spinning loader to let the user know something is happening.
214 | },
215 | complete: function () {
216 | $("#loadDiv").hide(); // Hide the spinning loader once the AJAX call is complete.
217 | },
218 | data: JSON.stringify({
219 | action: 'bundles'
220 | }),
221 | contentType: 'text/plain',
222 | error: function () {
223 | if (eventSource == "init") {
224 | determineWorkspace("recursive");
225 | } else {
226 | $('#reqBundle')
227 | .append($("
")
228 | .attr("value", "error")
229 | .text("ERROR: No bundles found."));
230 | $("#desktopNoExist").show(); // If no WorkSpace is returned, show the request panel.
231 | }
232 | },
233 | success: function (data) {
234 | for (var i = 0; i < data.Result.length; i++) {
235 | $('#reqBundle')
236 | .append($("
")
237 | .attr("value", data.Result[i].split(':')[0])
238 | .text(data.Result[i].split(':')[1]));
239 | $("#desktopNoExist").show(); // If no WorkSpace is returned, show the request panel.
240 | }
241 | }
242 | });
243 | },
244 | success: function (data) {
245 | // If a WorkSpace is returned, populate the table with its details (ID, Username, State, and Bundle ID).
246 | $("#workspace-Id").html(data.WorkspaceId);
247 | $("#workspace-Username").html(data.UserName);
248 | $("#workspace-State").html(data.State);
249 | $("#workspace-Bundle").html(data.BundleId);
250 | $("#desktopExist").show();
251 | }
252 | });
253 | }
254 |
255 | // the determineWorkflowStatus function is called to get the status details on the creation request. If the request is pending approval, the user is
256 | // notified of such. If the request is rejected, the user is notified of such until they acknowledge. If this request is approved or doesn't exist,
257 | // then nothing is displayed.
258 | function determineWorkflowStatus() {
259 | $.ajax({
260 | method: 'POST',
261 | url: WORKSPACES_CONTROL_URL,
262 | headers: {
263 | Authorization: authToken
264 | },
265 | beforeSend: function () {},
266 | complete: function () {},
267 | data: JSON.stringify({
268 | action: 'details'
269 | }),
270 | contentType: 'text/plain',
271 | error: function () {
272 |
273 | },
274 | success: function (data) {
275 |
276 | for (var i = 0; i < data.length; i++) {
277 | if (data[i].WS_Status.S == "Requested") {
278 | $("#methodStatus").append('
WorkSpace approval pending for user: ' + data[i].Username.S + '
');
279 |
280 | $("#methodStatus").show();
281 | } else if (data[i].WS_Status.S == "Rejected") {
282 | $("#methodStatus").append('
× WorkSpace request rejected for user: ' + data[i].Username.S + '
');
283 |
284 | $("#acknowledgeReject-" + data[i].Username.S).on('click', function () {
285 | $.ajax({
286 | method: 'POST',
287 | url: WORKSPACES_CONTROL_URL,
288 | headers: {
289 | Authorization: authToken
290 | },
291 | beforeSend: function () {},
292 | complete: function () {},
293 | data: JSON.stringify({
294 | action: 'acknowledge',
295 | username: this.id.split("-")[1]
296 | }),
297 | contentType: 'text/plain',
298 | error: function () {},
299 | success: function (data) {
300 | location.reload();
301 | }
302 | });
303 | });
304 |
305 | $("#methodStatus").show();
306 | }
307 | }
308 | }
309 | });
310 | }
311 |
312 | $(function init() {
313 |
314 | if (!_config.api.invokeUrl) { // Show this message if the Portal's API is not configured.
315 | $('#noApiMessage').show();
316 | }
317 |
318 | determineWorkflowStatus(); // Determine if there is an active request, and notify user of status.
319 | determineWorkspace("init"); // Determine if there is a WorkSpace assigned to the user, and if so then populate actions. If not, show the request div.
320 |
321 | });
322 |
323 | $(function onDocReady() {
324 |
325 | // Hook up functions to forms' submit buttons.
326 | $('#requestWorkSpace').submit(handleRequest);
327 | $('#decommissionWorkSpace').submit(handleDecommission);
328 | $('#confirmDecommissionWorkSpace').submit(handleConfirmDecommission);
329 | $('#rebootWorkSpace').submit(handleReboot);
330 | $('#rebuildWorkSpace').submit(handleRebuild);
331 |
332 | // If there is a WorkSpace for the user, give the user a direct button to refresh status.
333 | $("#reloadButton").on('click', function () {
334 | location.reload();
335 | });
336 |
337 | });
338 |
339 | }(jQuery));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WorkSpaces Portal
2 |
3 | **Note: This repository is no longer maintained.**
4 |
5 | The WorkSpaces Portal provides Self-Service capability to end-users for Amazon WorkSpaces virtual desktops. The portal provides the ability for users to create, rebuild, reboot, and delete their WorkSpace. The application is entirely serverless leveraging AWS Lambda, S3, API Gateway, Step Functions, Cognito, and SES. The application provides continuous deployment through AWS CodePipeline, CodeBuild, CloudFormation with SAM, and GitHub.
6 |
7 | ## Architecture
8 |
9 | 
10 |
11 | ### Components Overview
12 |
13 | This project leverages the following services:
14 |
15 | * [CloudFormation](https://aws.amazon.com/cloudformation/): Used to deploy the entire stack.
16 | * [AWS Serverless Application Model](https://aws.amazon.com/about-aws/whats-new/2016/11/introducing-the-aws-serverless-application-model/): Used to provision Lambda/API Gateway.
17 | * [S3](https://aws.amazon.com/s3/): Used to provide static website hosting and to store our build artifacts.
18 | * [Lambda](https://aws.amazon.com/lambda/): Used to perform Functions-as-a-Service. These can be tested with events in corresponding sample_events/ folder using [lambda-local](https://www.npmjs.com/package/lambda-local).
19 | * [API Gateway](https://aws.amazon.com/api-gateway/): Used to provide an integration point to our Lambda functions.
20 | * [Step Functions](https://aws.amazon.com/step-functions/): Used to provide a State Machine for Approval workflows.
21 | * [Cognito](https://aws.amazon.com/cognito/): Used to provide authentication for our website.
22 | * [SES](https://aws.amazon.com/ses/): Used to send Approval emails.
23 | * [CloudWatch Events](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html): Used to set a timer event for Lambda functions.
24 | * [IAM](https://aws.amazon.com/iam/): Provides security controls for our process.
25 | * [CloudFront](https://aws.amazon.com/cloudfront/): Provides HTTPS in front of S3 web site.
26 | * [CodePipeline](https://aws.amazon.com/codepipeline/): Used to provide the pipeline functionality for our CI/CD process.
27 | * [Code Build](https://aws.amazon.com/codebuild/): Used to build the project as part of CodePipeline process.
28 | * [GitHub](http://www.github.com): Used as the source code repository. Could theoretically be replaced with CodeCommit.
29 | * [Jekyll](http://www.jekyllrb.com): Provides static web site generation to convert the `website/` directory.
30 |
31 | ## Usage
32 |
33 | ### User Account Creation
34 |
35 | Users can create their accounts through the register page. Anyone with an email on the Approved Domain as specified in the stack can register.
36 |
37 | 
38 |
39 | After registering, users will receive a verification token through email. The user must enter this token on the verification page. Users are automatically redirected to the verify page after registering; however, they can also access it by accessing the site and browsing the verify from the top-right dropdown.
40 |
41 | 
42 |
43 | Once verified, the user can sign in to the portal with their created credentials.
44 |
45 | ### Creating a WorkSpace
46 |
47 | Upon signing in, they will see the WorkSpace Request form as they have not created a WorkSpace yet. They can submit a request which will start the Approval process.
48 |
49 | 
50 |
51 | The Approver email as specified within the stack will receive an email with links to Approve or Reject the request.
52 |
53 | Upon signing in, they will see the WorkSpace Request form as they have not created a WorkSpace yet.
54 |
55 | 
56 |
57 | Once approved, the WorkSpace will begin automatically and immediately.
58 |
59 | ### Managing a WorkSpace
60 |
61 | After the WorkSpace is provisioned, the user will receive an email directly from Amazon with details on how to access their WorkSpace.
62 |
63 | 
64 |
65 | They can also begin managing the WorkSpace through the portal: rebuild, reboot, or delete.
66 |
67 | 
68 |
69 | ## Deployment
70 |
71 | ### Prerequisites
72 |
73 | What things you need to install the software and how to install them
74 |
75 | 1. The AWS account must be setup for SES for production usage. By default, SES is locked down, and needs to be [moved out of the Amazon SES Sandbox](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html).
76 | 2. Amazon WorkSpaces at the most basic level should be setup. This includes setting up a Directory Services directory and registering it for WorkSpaces.
77 |
78 | ### GitHub Creation
79 |
80 | 1. Create a GitHub repository that mirrors the project repository.
81 | 2. If the repository is private, navigate to https://github.com/settings/tokens/new to generate a new Personal Access Token for the pipeline to use.
82 | 3. Give the token an appropriate name, and select: `repo` and `admin:repo_hook`.
83 | 4. Save the Personal Access Token as it will be entered into the CloudFormation deployment.
84 |
85 | ### CloudFormation
86 |
87 | Deploying the application starts by running the `deploy.json` file inside CloudFormation. The `deploy.json` template will ask for the following parameters:
88 |
89 | 1. **AppName**: Name of the application that will be used in some components naming scheme.
90 | 2. **BucketName**: Name of the S3 Bucket to create that should house the website. This must be unique within the S3 namespace.
91 | 3. **CognitoPool**: Name of the Cognito Pool to create to use for authentication purposes.
92 | 4. **SAMInputFile**: Serverless transform file. By default, this is the included `wsportal.json` file. (Don't change unless renaming wsportal.json)
93 | 5. **SAMOutputFile**: The filename for the output file from the buildspec file. (This doesn't need to be changed unless the artifact file inside the `buildspec.yml` file is changed to a different name.)
94 | 6. **CodeBuildImage**: Name of the CodeBuild container image to use. (Don't change unless willing to edit buildspec.yml accordingly.)
95 | 7. **GitHubRepName**: Name of the GitHub repo that houses the application code.
96 | 8. **GitHubRepoBranch**: Branch of the GitHub repo that houses the application code.
97 | 9. **GitHubUser**: GitHub Username.
98 | 10. **GitHubToken**: GitHub token to use for authentication to the GitHub account. Configurable inside Github: https://github.com/settings/tokens. Token needs `repo_hook` permissions.
99 |
100 | The CloudFormation deployment will notify that it will create IAM Permissions; check the box acknowledging the permissions creation to proceed.
101 |
102 | The files referenced (e.g. SAMInputFile) are expected to exist within the GitHub repository. The CloudFormation deployment will warn that it is creating IAM permissions; this is because it creates roles and policies for the pipeline to use when it creates/modifies the child stack.
103 |
104 | The initial CloudFormation Stack should be created after `deploy.json` is launched. Once that stack is created, the CodePipeline will then create the child stack after a short period of time. The child stack will be called ``{parent-stack}-serverless-stack``.
105 |
106 | Once deployed, the application still requires some additional configuration to work.
107 |
108 | ### Configuration
109 |
110 | After initial deployment, the site will not be fully functional as a few configuration steps must occur. Before proceeding, let CodePipeline perform an initial deployment of the child stack. This is needed to be able to select the Lambda functions created within the API Gateway created in the next step. As soon as the child stack appears within the CloudFormation Stacks list, configuration steps below can begin.
111 |
112 | #### Manually Create API Gateway
113 |
114 | The Serverless Application Model (SAM) within AWS / CloudFormation does not support enabling CORS directly. As such, if using the API Gateway created through that method, it will not work and will consistently throw CORS errors in the brower. There is an [open issue](https://github.com/awslabs/serverless-application-model/issues/23) on the SAM GitHub repo, and this will hopefully be added soon in the future.
115 |
116 | In the meantime, the API Gateway is built manually. To create the API Gateway manually, follow these steps:
117 |
118 | 1. Navigate to API Gateway within the AWS Console
119 | 2. Select `Create API`
120 | 3. Select `New API`
121 | 4. Enter an API Name (e.g. workspaces-portal)
122 | 5. Select `Create API`
123 | 6. Select `Actions` dropdown -> `Create Resource`
124 | 7. Enter `workspaces-control` as the Resource name.
125 | 8. Check `Enable API Gateway CORS`
126 | 9. Select `Create Resource`
127 | 10. Select `Actions` dropdown -> `Create Method`
128 | 11. From the dropdown under the resource, choose `POST` then select the checkmark.
129 | 12. Check `Use Lambda Proxy Integration`
130 | 13. Choose the region the environment is deployed within from the dropdown.
131 | 14. Select the workspacesControl Lambda that has previously been created by CloudFormation (e.g. appName-serverless-stack-workspacesControl-ABCDEFGH).
132 | 15. Select `Save`
133 | 16. When the `Add Permissions` pop-up opens, select `OK`.
134 | 17. Select `Authorizers` on the left pane under the API.
135 | 18. Select `Create New Authorizer`.
136 | 19. Enter a name for the Authorizer (e.g. portal_cognito).
137 | 20. Under `Type` select Cognito.
138 | 21. Select the pool created by CloudFormation (e.g. appName-pool).
139 | 22. Under `Token Source`, enter `Authorization`.
140 | 23. Select `Create`.
141 | 24. Select `Resources` on the left pane under the API.
142 | 25. Select `POST` under the `workspaces-control` resource.
143 | 26. Select `Method Request`.
144 | 27. Select the pencil next to `Authorization` and then select the Authorizer previously created manually (e.g. portal_cognito), and select the checkmark. This may require refreshing the page if only only `AWS_IAM` appears.
145 | 28. Select `Action` dropdown -> `Deploy API`.
146 | 29. Under `Deployment Stage`, select `[New Stage]`.
147 | 30. Under `Stage name`, enter `Prod`.
148 | 31. Select `Deploy`.
149 | 32. Copy the `Invoke URL` (e.g. https://abcdefgh.execute-api.us-west-2.amazonaws.com/Prod).
150 | 33. Paste the `Invoke URL` into `website/js/config.js` within `api.invokeUrl`.
151 |
152 | #### Update Web Config with Infrastructure Details
153 |
154 | The `config.js` file within `website/js/` needs to be updated so the site knows how to utilize the services (this is a one-time process).
155 |
156 | Within the parent Stack, the Outputs tab should display the following items:
157 |
158 | 1. **UserPoolClientId**
159 | 2. **BucketName**
160 | 3. **UserPoolId**
161 | 4. **OriginURL**
162 |
163 | The `UserPoolClientId` and `UserPoolId` should be placed into the `website/js/config.js` file within the `cognito.userPoolId` and `cognito.userPoolClientId` so that the website knows how to use the services provisioned. If not using `us-east-1`, also change the region within `cognito.region` accordingly.
164 |
165 | #### Configure Approval Email Address
166 |
167 | For WorkSpace creation approvals, configure the email address within the `approval.email` entry inside `website/js/config.js`. This could be an individual address or a Distribution List.
168 |
169 | #### Configure Cognito to use Custom Trigger
170 |
171 | *Warning: If this is not configured, anyone can sign up and use the portal.*
172 |
173 | Also needed after deployment is to configure Cognito to use the cognito-domainVerify Lambda. To configure manually, follow these steps:
174 |
175 | 1. Navigate to Cognito within the AWS Console.
176 | 2. Select `Manage your User Pools`.
177 | 3. Select the pool created by CloudFormation (e.g. appName-pool).
178 | 4. Select `Triggers` under `General Settings`.
179 | 5. Under `Custom message`, select the cognito-domainVerify Lambda (e.g. AppName-serverless-stack-cogDomainVerify-ABCDEFGH).
180 | 6. Select `Save Changes`.
181 |
182 | This will enable limiting signups to the email domain configured in the function.
183 |
184 | #### Configure App Settings
185 |
186 | Edit `wsportal.json` to update the settings specific to your environment. Simply change the `Default` values under the `Parameters` section.
187 |
188 | 1. **AppName**: This name will be used within the application components.
189 | 2. **PortalEmail**: This is the email address that approval emails will be sent from.
190 | 3. **ApproverEmail**: This is the email or distribution list that will receive the Approval email messages to approve or reject.
191 | 4. **DirectoryServicesId**: This is the Directory Services ID configured within Amazon WorkSpaces.
192 | 5. **ApprovedDomain**: This is the domain that can sign up for the portal (e.g. @company.com).
193 |
194 | #### Push Changes
195 |
196 | Once the `config.js` file is updated, push the change to the GitHub repo; this will automatically update the application with the new config through the pipeline.
197 |
198 | #### Create CloudFront Web Distribution to provide HTTPS Support
199 |
200 | In order to provide HTTPS coverage for the portal, create a CloudFront Web Distribution. This could be created automatically, but it's unlikely that a *.cloudfront.net address is desired. Instead, that is left up to the administrator to manually deploy using their desired domain name with a related SSL certificate.
201 |
202 | ### Testing
203 |
204 | The site should now work as expected. Browse to the URL defined within `OriginURL` output of the parent CloudFormation stack (e.g. http://s3bucketportalname.s3-website-us-east-1.amazonaws.com), and select "Register" from the top right drop-down (Please note that this will be over HTTP and unencrypted at this point). Enter an email address (within the configured domain inside cogDomainVerify) and password, and select Register. You will receive a verification code from Cognito through email. Once the email is received with the token, select "Verify" from the top right drop-down; on the verify page, enter your email and the verification code provided. At this point, the site will redirect to login. Login with the authentication credentials created. To ensure only users within the specified domain can register for the portal, test registering with an email address on an unapproved domain.
205 |
206 | On the main page, the ability to request a WorkSpace should now be displayed. Request a WorkSpace with a user (which must exist within the Directory; only include the username itself without any domain information within it) and select a Bundle to use. It should begin the approval process, and the email address configured for approvals should receive an Approval Request email within approximately 10 minutes, which is within time for the CloudWatch Event that triggers the Lambda function polling Step Functions to run (this can be configured lower within `wsportal.json` for the Lambda function if desired. Approve the request, and the creation process should begin.
207 |
208 | The user should receive an email with instructions on how to use their WorkSpace once it finishes provisioning. The user can also log back into the WorkSpaces Portal to try rebooting, rebuilding, or deleting the WorkSpace. Test the ability to perform an activity under WorkSpace Operations on the provisioned WorkSpace.
209 |
210 | If everything above works, the application deployed successfully.
211 |
212 | ## Removal
213 |
214 | 1. CloudFormation -> Delete Child Stack (e.g. appname-serverless-stack).
215 | 2. CloudFormation -> Delete Parent Stack (e.g. appname)
216 | 3. Delete all WorkSpaces created (if desired).
217 | 4. Delete the Directory Services directory (if desired).
218 | 5. Delete the `workspaces-portal` API Gateway.
219 | 6. Delete the created S3 Buckets (e.g. AppName-Bucket & serverless-app-
--AppName).
220 | 7. Delete the CloudFront Web Distribution.
221 |
222 | ## Updating
223 |
224 | As the website or serverless function is updated, simply perform the modifications within the code and then push them to the GitHub repo. Once checked in to GitHub, CodePipeline will handle the rest automatically. To test this functionality, browse to the CodePipeline page and view the pipeline while pushing a change. The pipeline will show the process from Source -> Build -> Deploy. If there are any failures, they will be visible within the pipeline.
225 |
226 | The code for the pipeline resides within the root of the project, and the pipeline itself exists as part of the parent CloudFormation stack:
227 |
228 | 1. **deploy.json**: Launcher for the core services within CloudFormation (S3, CodePipeline, CodeBuild, Cognito). These are not modified by the pipeline on changes, but it does include setting up the pipeline itself. This is the CloudFormation template to launch to get setup started.
229 | 2. **buildspec.yml**: This file is used by CodeBuild to tell it what to do on every build, such as running jekyll and copying the output to S3.
230 | **wsportal.json**: CloudFormation Serverless Transformation template for SAM. This template handles creation of the Lambda functions, Step Functions, and the approval API Gateway.
231 |
232 | ## Notes
233 |
234 | 1. If you want to delete the stack, make sure to delete the pipeline-created stack first and then delete the parent stack. If you delete the parent first, the IAM role is deleted and you'll have to tinker around with permissions to get the stack to actually gracefully delete.
235 | 2. Some of the IAM permissions may be more liberal than preferred. Please review and edit to match to your security policies as appropriate.
236 |
237 | ## Authors
238 |
239 | * **Earl Gay** - *Initial work* - [eeg3](https://github.com/eeg3)
240 |
241 | See also the list of [contributors](https://github.com/eeg3/workspaces-portal/contributors) who participated in this project.
242 |
243 | ## License
244 |
245 | This project is licensed under the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause).
246 |
247 | ## Acknowledgments
248 |
249 | * [AWS Labs: Severless Web Application WorkShop](https://github.com/awslabs/aws-serverless-workshops/tree/master/WebApplication/)
250 | * [AWS Labs: Serverless SAM Farm](https://github.com/awslabs/aws-serverless-samfarm)
251 | * [AWS Compute Blog: Implementing Serverless Manual Approval Steps in AWS Step Functions and Amazon API Gateway](https://aws.amazon.com/blogs/compute/implementing-serverless-manual-approval-steps-in-aws-step-functions-and-amazon-api-gateway/)
252 |
--------------------------------------------------------------------------------
/wsportal.json:
--------------------------------------------------------------------------------
1 | {
2 | "Transform": "AWS::Serverless-2016-10-31",
3 | "Description": "WorkSpaces Portal Application Stack",
4 | "Parameters": {
5 | "OriginUrl": {
6 | "Description": "The origin url to allow CORS requests from. This will be the base URL of the static website.",
7 | "Type": "String",
8 | "Default": "*"
9 | },
10 | "AppName": {
11 | "Type": "String",
12 | "Description": "Name of the portal.",
13 | "MinLength": "1",
14 | "MaxLength": "80",
15 | "AllowedPattern": "[A-Za-z0-9-]+",
16 | "ConstraintDescription": "Malformed input parameter. AppName must only contain upper and lower case letters, numbers, and -.",
17 | "Default": "wsPortal"
18 | },
19 | "PortalEmail": {
20 | "Type": "String",
21 | "Description": "Address from which emails will be sent from to approvers.",
22 | "Default": "approval@roundtower.io"
23 | },
24 | "ApproverEmail": {
25 | "Type": "String",
26 | "Description": "Email address that will receive WorkSpace Creation approval requests.",
27 | "Default": "earl@eeg3.net"
28 | },
29 | "DirectoryServicesId": {
30 | "Type": "String",
31 | "Description": "Directory Services ID configured within Amazon WorkSpaces.",
32 | "Default": "d-90672a878e"
33 | },
34 | "ApprovedDomain": {
35 | "Type": "String",
36 | "Description": "Approved email domain that can sign up for Portal access.",
37 | "Default": "eeg3.net"
38 | }
39 | },
40 | "Resources": {
41 | "cogDomainVerify": {
42 | "Type": "AWS::Serverless::Function",
43 | "Properties": {
44 | "Handler": "cogDomainVerify.handler",
45 | "Runtime": "nodejs6.10",
46 | "CodeUri": "./lambda/cognito-domainVerify/",
47 | "Environment": {
48 | "Variables": {
49 | "APPROVED_DOMAIN": {
50 | "Ref": "ApprovedDomain"
51 | }
52 | }
53 | }
54 | }
55 | },
56 | "workspacesControl": {
57 | "Type": "AWS::Serverless::Function",
58 | "Properties": {
59 | "Handler": "workspaces-control.handler",
60 | "Runtime": "nodejs6.10",
61 | "CodeUri": "./lambda/workspaces-control/",
62 | "Timeout": 5,
63 | "Policies": ["AmazonWorkSpacesAdmin", "AWSStepFunctionsFullAccess", "AWSLambdaRole"],
64 | "Environment": {
65 | "Variables": {
66 | "ORIGIN_URL": {
67 | "Ref": "OriginUrl"
68 | },
69 | "STATE_MACHINE_ARN": {
70 | "Ref": "approvalStateMachine"
71 | },
72 | "DETAILS_LAMBDA": {
73 | "Ref": "workspacesDetails"
74 | }
75 | }
76 | }
77 | }
78 | },
79 | "workspacesCreate": {
80 | "Type": "AWS::Serverless::Function",
81 | "Properties": {
82 | "Handler": "workspaces-create.handler",
83 | "Runtime": "nodejs6.10",
84 | "CodeUri": "./lambda/workspaces-create/",
85 | "Policies": "AmazonWorkSpacesAdmin",
86 | "Environment": {
87 | "Variables": {
88 | "ORIGIN_URL": {
89 | "Ref": "OriginUrl"
90 | },
91 | "DIRECTORY_ID": {
92 | "Ref": "DirectoryServicesId"
93 | }
94 | }
95 | }
96 | }
97 | },
98 | "workspacesAppoval": {
99 | "Type": "AWS::Serverless::Function",
100 | "Properties": {
101 | "Handler": "workspaces-approval.handler",
102 | "Runtime": "nodejs6.10",
103 | "CodeUri": "./lambda/workspaces-approval/",
104 | "Timeout": 75,
105 | "Policies": "AdministratorAccess",
106 | "Environment": {
107 | "Variables": {
108 | "TASK_ARN": {
109 | "Ref": "approvalManualActivity"
110 | },
111 | "API_DEPLOYMENT_ID": {
112 | "Ref": "approvalResponderAPI"
113 | },
114 | "FROM_ADDRESS": {
115 | "Ref": "PortalEmail"
116 | },
117 | "APPROVER_EMAIL_ADDRESS": {
118 | "Ref": "ApproverEmail"
119 | }
120 | }
121 | },
122 | "Events": {
123 | "Timer": {
124 | "Type": "Schedule",
125 | "Properties": {
126 | "Schedule": "rate(5 minutes)"
127 | }
128 | }
129 | }
130 | }
131 | },
132 | "workspacesDetails": {
133 | "Type": "AWS::Serverless::Function",
134 | "DependsOn": "WorkspaceDetailsTable",
135 | "Properties": {
136 | "Handler": "workspaces-details.handler",
137 | "Runtime": "nodejs6.10",
138 | "CodeUri": "./lambda/workspaces-details/",
139 | "Policies": "AmazonDynamoDBFullAccess",
140 | "Environment": {
141 | "Variables": {
142 | "ORIGIN_URL": {
143 | "Ref": "OriginUrl"
144 | },
145 | "DETAILS_TABLE_NAME": {
146 | "Ref": "WorkspaceDetailsTable"
147 | }
148 | }
149 | }
150 | }
151 | },
152 | "approvalStepFunctionsRole": {
153 | "Description": "Creating service role in IAM for Step Functions",
154 | "Type": "AWS::IAM::Role",
155 | "Properties": {
156 | "RoleName": {
157 | "Fn::Sub": "${AppName}-${AWS::Region}-sfn-role"
158 | },
159 | "AssumeRolePolicyDocument": {
160 | "Statement": [{
161 | "Effect": "Allow",
162 | "Principal": {
163 | "Service": [{
164 | "Fn::Sub": "states.${AWS::Region}.amazonaws.com"
165 | }]
166 | },
167 | "Action": "sts:AssumeRole"
168 | }]
169 | },
170 | "Path": "/",
171 | "Policies": [{
172 | "PolicyName": "workspaceCreateInvoke",
173 | "PolicyDocument": {
174 | "Statement": [{
175 | "Effect": "Allow",
176 | "Action": "lambda:InvokeFunction",
177 | "Resource": [{
178 | "Fn::GetAtt": ["workspacesCreate", "Arn"]
179 | },
180 | {
181 | "Fn::GetAtt": ["workspacesDetails", "Arn"]
182 | }]
183 | }]
184 | }
185 | }]
186 | }
187 | },
188 | "approvalAPIGatewayRole": {
189 | "Type": "AWS::IAM::Role",
190 | "Properties": {
191 | "RoleName": {
192 | "Fn::Sub": "${AppName}-${AWS::Region}-apigw-role"
193 | },
194 | "AssumeRolePolicyDocument": {
195 | "Statement": [{
196 | "Effect": "Allow",
197 | "Principal": {
198 | "Service": "apigateway.amazonaws.com"
199 | },
200 | "Action": "sts:AssumeRole"
201 | }]
202 | },
203 | "ManagedPolicyArns": [
204 | "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
205 | "arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess"
206 | ]
207 | }
208 | },
209 | "approvalManualActivity": {
210 | "Type": "AWS::StepFunctions::Activity",
211 | "Properties": {
212 | "Name": {
213 | "Fn::Sub": "${AppName}-Step"
214 | }
215 | }
216 | },
217 | "approvalStateMachine": {
218 | "Type": "AWS::StepFunctions::StateMachine",
219 | "Properties": {
220 | "StateMachineName": {
221 | "Fn::Sub": "${AppName}-StateMachine"
222 | },
223 | "DefinitionString": {
224 | "Fn::Join": ["", ["{ \"StartAt\": \"WorkspaceApproval\", \"States\": { \"WorkspaceApproval\": { \"Type\": \"Parallel\", \"Branches\": [{ \"StartAt\": \"approvalManualActivity\", \"States\": { \"approvalManualActivity\": { \"Type\": \"Task\", \"Resource\": \"",
225 | {
226 | "Ref": "approvalManualActivity"
227 | },
228 | "\", \"TimeoutSeconds\": 3600, \"Catch\": [{ \"ErrorEquals\": [\"States.TaskFailed\"], \"Next\": \"updateFinalWSDetailStatus\" }], \"Next\": \"workspacesCreate\" }, \"workspacesCreate\": { \"Type\": \"Task\", \"Resource\": \"",
229 | {
230 | "Fn::GetAtt": ["workspacesCreate", "Arn"]
231 | },
232 | "\", \"Next\": \"updateFinalWSDetailStatus\" }, \"updateFinalWSDetailStatus\": { \"Type\": \"Task\", \"Resource\": \"",
233 | {
234 | "Fn::GetAtt": ["workspacesDetails", "Arn"]
235 | },
236 | "\", \"End\": true } } }, { \"StartAt\": \"updateInitialWSDetailStatus\", \"States\": { \"updateInitialWSDetailStatus\": { \"Type\": \"Task\", \"Resource\": \"",
237 | {
238 | "Fn::GetAtt": ["workspacesDetails", "Arn"]
239 | },
240 | "\", \"End\": true } } }], \"End\": true } } }"
241 | ]]
242 | },
243 | "RoleArn": {
244 | "Fn::GetAtt": ["approvalStepFunctionsRole", "Arn"]
245 | }
246 | }
247 | },
248 | "approvalResponderAPI": {
249 | "Type": "AWS::ApiGateway::RestApi",
250 | "Properties": {
251 | "Body": {
252 | "swagger": "2.0",
253 | "info": {
254 | "version": "2017-12-20T19:01:15Z",
255 | "title": "ApprovalResponderAPI"
256 | },
257 | "basePath": "/respond",
258 | "schemes": [
259 | "https"
260 | ],
261 | "paths": {
262 | "/fail": {
263 | "get": {
264 | "consumes": [
265 | "application/json"
266 | ],
267 | "produces": [
268 | "application/json"
269 | ],
270 | "parameters": [{
271 | "name": "taskToken",
272 | "in": "query",
273 | "required": false,
274 | "type": "string"
275 | }],
276 | "responses": {
277 | "200": {
278 | "description": "200 response",
279 | "schema": {
280 | "$ref": "#/definitions/Empty"
281 | }
282 | }
283 | },
284 | "x-amazon-apigateway-integration": {
285 | "credentials": {
286 | "Fn::GetAtt": ["approvalAPIGatewayRole", "Arn"]
287 | },
288 | "responses": {
289 | "default": {
290 | "statusCode": "200"
291 | }
292 | },
293 | "uri": {
294 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:states:action/SendTaskFailure"
295 | },
296 | "passthroughBehavior": "when_no_templates",
297 | "httpMethod": "POST",
298 | "requestTemplates": {
299 | "application/json": "{\n \"error\": \"Rejected\",\n \"cause\": \"$input.params('requesterEmailAddress'),$input.params('requesterUsername')\",\n \"taskToken\": \"$input.params('taskToken')\"\n}"
300 | },
301 | "type": "aws"
302 | }
303 | },
304 | "options": {
305 | "consumes": [
306 | "application/json"
307 | ],
308 | "produces": [
309 | "application/json"
310 | ],
311 | "responses": {
312 | "200": {
313 | "description": "200 response",
314 | "schema": {
315 | "$ref": "#/definitions/Empty"
316 | },
317 | "headers": {
318 | "Access-Control-Allow-Origin": {
319 | "type": "string"
320 | },
321 | "Access-Control-Allow-Methods": {
322 | "type": "string"
323 | },
324 | "Access-Control-Allow-Headers": {
325 | "type": "string"
326 | }
327 | }
328 | }
329 | },
330 | "x-amazon-apigateway-integration": {
331 | "responses": {
332 | "default": {
333 | "statusCode": "200",
334 | "responseParameters": {
335 | "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
336 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'",
337 | "method.response.header.Access-Control-Allow-Origin": "'*'"
338 | }
339 | }
340 | },
341 | "passthroughBehavior": "when_no_match",
342 | "requestTemplates": {
343 | "application/json": "{\"statusCode\": 200}"
344 | },
345 | "type": "mock"
346 | }
347 | }
348 | },
349 | "/succeed": {
350 | "get": {
351 | "consumes": [
352 | "application/json"
353 | ],
354 | "produces": [
355 | "application/json"
356 | ],
357 | "responses": {
358 | "200": {
359 | "description": "200 response",
360 | "schema": {
361 | "$ref": "#/definitions/Empty"
362 | }
363 | }
364 | },
365 | "x-amazon-apigateway-integration": {
366 | "credentials": {
367 | "Fn::GetAtt": ["approvalAPIGatewayRole", "Arn"]
368 | },
369 | "responses": {
370 | "default": {
371 | "statusCode": "200"
372 | }
373 | },
374 | "uri": {
375 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:states:action/SendTaskSuccess"
376 | },
377 | "passthroughBehavior": "when_no_templates",
378 | "httpMethod": "POST",
379 | "requestTemplates": {
380 | "application/json": "{\n \"output\": \"\\\"$input.params('requesterEmailAddress'),$input.params('requesterUsername'),$input.params('requesterBundle')\\\"\",\n \"taskToken\": \"$input.params('taskToken')\"\n}"
381 | },
382 | "type": "aws"
383 | }
384 | },
385 | "options": {
386 | "consumes": [
387 | "application/json"
388 | ],
389 | "produces": [
390 | "application/json"
391 | ],
392 | "responses": {
393 | "200": {
394 | "description": "200 response",
395 | "schema": {
396 | "$ref": "#/definitions/Empty"
397 | },
398 | "headers": {
399 | "Access-Control-Allow-Origin": {
400 | "type": "string"
401 | },
402 | "Access-Control-Allow-Methods": {
403 | "type": "string"
404 | },
405 | "Access-Control-Allow-Headers": {
406 | "type": "string"
407 | }
408 | }
409 | }
410 | },
411 | "x-amazon-apigateway-integration": {
412 | "responses": {
413 | "default": {
414 | "statusCode": "200",
415 | "responseParameters": {
416 | "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
417 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'",
418 | "method.response.header.Access-Control-Allow-Origin": "'*'"
419 | }
420 | }
421 | },
422 | "passthroughBehavior": "when_no_match",
423 | "requestTemplates": {
424 | "application/json": "{\"statusCode\": 200}"
425 | },
426 | "type": "mock"
427 | }
428 | }
429 | }
430 | },
431 | "definitions": {
432 | "Empty": {
433 | "type": "object",
434 | "title": "Empty Schema"
435 | }
436 | }
437 | },
438 | "Description": "Approval Responder API",
439 | "Name": "approvalResponderAPI"
440 | }
441 | },
442 | "approvalApiDeployment": {
443 | "Type": "AWS::ApiGateway::Deployment",
444 | "Properties": {
445 | "RestApiId": {
446 | "Ref": "approvalResponderAPI"
447 | },
448 | "Description": "Respond Deployment",
449 | "StageName": "respond"
450 | }
451 | },
452 | "WorkspaceDetailsTable": {
453 | "Type": "AWS::DynamoDB::Table",
454 | "Properties": {
455 | "AttributeDefinitions": [{
456 | "AttributeName": "Email",
457 | "AttributeType": "S"
458 | },
459 | {
460 | "AttributeName": "Username",
461 | "AttributeType": "S"
462 | }
463 | ],
464 | "KeySchema": [{
465 | "AttributeName": "Email",
466 | "KeyType": "HASH"
467 | },
468 | {
469 | "AttributeName": "Username",
470 | "KeyType": "RANGE"
471 | }
472 | ],
473 | "ProvisionedThroughput": {
474 | "ReadCapacityUnits": "5",
475 | "WriteCapacityUnits": "5"
476 | }
477 | }
478 | }
479 | },
480 | "Outputs": {
481 | "WorkspaceDetailsTable": {
482 | "Value": {
483 | "Ref": "WorkspaceDetailsTable"
484 | },
485 | "Description": "Table name for WorkSpace Details"
486 | }
487 | }
488 | }
--------------------------------------------------------------------------------
/lambda/workspaces-control/workspaces-control.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Load the AWS SDK for Node.js
4 | var AWS = require("aws-sdk");
5 |
6 | // Create the WorkSpaces service object
7 | var workspaces = new AWS.WorkSpaces({
8 | apiVersion: "2015-04-08"
9 | });
10 |
11 | // Create the Step Functions service object
12 | var stepfunctions = new AWS.StepFunctions();
13 |
14 | // Create the Lambda service object
15 | var lambda = new AWS.Lambda();
16 |
17 | exports.handler = (event, context, callback) => {
18 | var originURL = process.env.ORIGIN_URL || "*"; // Origin URL to allow for CORS
19 | var stateMachine =
20 | process.env.STATE_MACHINE_ARN ||
21 | "arn:aws:states:us-east-1:375301133253:stateMachine:PromotionApproval"; // State Machine for 'create' action.
22 | var detailsLambda =
23 | process.env.DETAILS_LAMBDA ||
24 | "wsp-db-int-serverless-stack-workspacesDetails-1J4ZB3URZF2QP";
25 |
26 | console.log("Received event:", JSON.stringify(event, null, 2)); // Output log for debugging purposes.
27 |
28 | // The 'action' parameter specifies what workspaces control should do. Accepted values: list, acknowledge, create, rebuild, reboot, delete, bundles.
29 | var action = JSON.parse(event.body)["action"];
30 | console.log("action: " + action);
31 |
32 | if (action == "list") {
33 | // 'list' handles outputting the WorkSpace details assigned to the user that submits the API call.
34 | // If no workspace is found, currently just responds with an error which is handled client-side.
35 |
36 | // The 'email' value within the Cognito token is used to determine ownership, which is checked agaisnt the 'SelfServiceManaged' tag value.
37 | // The tag value is used for ownership detection in order to avoid integrating with Directory Services directly.
38 | console.log(
39 | "Trying to find desktop owned by: " +
40 | event.requestContext.authorizer.claims.email
41 | );
42 |
43 | var params = [];
44 |
45 | // Obtain a list of all WorkSpaces, then parse the returned list to find the one with a 'SelfServiceManaged' tag
46 | // that equals the email address of the Cognito token, then take the ID of that WorkSpace and return all of its details back.
47 | workspaces.describeWorkspaces(describeWorkspacesParams, function (
48 | err,
49 | data
50 | ) {
51 | if (err) {
52 | console.log(err, err.stack); // an error occurred
53 | } else {
54 | for (var i = 0; i < data.Workspaces.length; i++) {
55 | var workspaceDetails = data[i];
56 | var describeTagsParams = {
57 | ResourceId: data.Workspaces[i].WorkspaceId
58 | };
59 | (function (describeTagsParams) { // Limit scope to avoid describeTagsParams getting overwritten too fast.
60 | workspaces.describeTags(describeTagsParams, function (err, data) {
61 | if (err) {
62 | console.log(err, err.sZtack);
63 | } else {
64 | for (var i = 0; i < data.TagList.length; i++) {
65 | console.log(
66 | "describeTagsParams.ResourceId: " +
67 | describeTagsParams.ResourceId
68 | );
69 |
70 | if (
71 | data.TagList[i].Key == "SelfServiceManaged" &&
72 | data.TagList[i].Value ==
73 | event.requestContext.authorizer.claims.email
74 | ) {
75 | console.log(
76 | "Desktop for '" +
77 | event.requestContext.authorizer.claims.email +
78 | "' found: " +
79 | describeTagsParams.ResourceId
80 | );
81 |
82 | var describeDetailsParams = {
83 | WorkspaceIds: [describeTagsParams.ResourceId]
84 | };
85 |
86 | workspaces.describeWorkspaces(
87 | describeDetailsParams,
88 | function (err, data) {
89 | if (err) {
90 | console.log(err, err.stack);
91 | } else {
92 | callback(null, {
93 | statusCode: 200,
94 | body: JSON.stringify(data.Workspaces[0]),
95 | headers: {
96 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
97 | "Access-Control-Allow-Methods": "GET,OPTIONS",
98 | "Access-Control-Allow-Origin": originURL
99 | }
100 | });
101 | }
102 | }
103 | );
104 | }
105 | }
106 | }
107 | });
108 | })(describeTagsParams);
109 | }
110 | }
111 | });
112 | } else if (action == "details") {
113 | var payloadString = JSON.stringify({
114 | action: "get",
115 | requesterEmailAddress: event.requestContext.authorizer.claims.email
116 | });
117 | var detailsParams = {
118 | FunctionName: detailsLambda,
119 | Payload: JSON.stringify({
120 | body: payloadString
121 | })
122 | };
123 |
124 | lambda.invoke(detailsParams, function (err, data) {
125 | if (err) {
126 | console.log(err, err.stack);
127 | } else {
128 | console.log("Data: " + JSON.stringify(data));
129 |
130 | console.log("Payload: " + data.Payload);
131 |
132 | for (var i = 0; i < JSON.parse(data.Payload).length; i++) {
133 | console.log(
134 | "Username #" + i + ": " + JSON.parse(data.Payload)[i].Username.S
135 | );
136 | }
137 |
138 | callback(null, {
139 | statusCode: 200,
140 | body: data.Payload,
141 | headers: {
142 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
143 | "Access-Control-Allow-Methods": "GET,OPTIONS",
144 | "Access-Control-Allow-Origin": originURL
145 | }
146 | });
147 | }
148 | });
149 | } else if (action == "acknowledge") {
150 | var payloadString = JSON.stringify({
151 | action: "put",
152 | requesterEmailAddress: event.requestContext.authorizer.claims.email,
153 | requesterUsername: JSON.parse(event.body)["username"],
154 | ws_status: "Acknowledged"
155 | });
156 |
157 | var ackParams = {
158 | FunctionName: detailsLambda,
159 | Payload: JSON.stringify({
160 | body: payloadString
161 | })
162 | };
163 |
164 | lambda.invoke(ackParams, function (err, data) {
165 | if (err) {
166 | console.log(err, err.stack);
167 | } else {
168 | console.log("Data: " + JSON.stringify(data));
169 |
170 | callback(null, {
171 | statusCode: 200,
172 | body: data.Payload,
173 | headers: {
174 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
175 | "Access-Control-Allow-Methods": "GET,OPTIONS",
176 | "Access-Control-Allow-Origin": originURL
177 | }
178 | });
179 | }
180 | });
181 | } else if (action == "create") {
182 | // 'create' handles creation by initiating the Step Functions State Machine. The State Machine first sends an email
183 | // to the configured Approver email address with two links: one to approve and one to decline. If the Approver declines,
184 | // the process ends. If the Approver approves, the next State Machine calls another Lambda function 'workspaces-create' that
185 | // actually handles creating the WorkSpace.
186 |
187 | var stepParams = {
188 | stateMachineArn: stateMachine,
189 | /* required */
190 | input: JSON.stringify({
191 | action: "put",
192 | requesterEmailAddress: event.requestContext.authorizer.claims.email,
193 | requesterUsername: JSON.parse(event.body)["username"],
194 | requesterBundle: JSON.parse(event.body)["bundle"],
195 | ws_status: "Requested"
196 | })
197 | };
198 | stepfunctions.startExecution(stepParams, function (err, data) {
199 | if (err) {
200 | console.log(err, err.stack);
201 | } else {
202 | console.log(data);
203 | callback(null, {
204 | statusCode: 200,
205 | body: JSON.stringify({
206 | Result: data
207 | }),
208 | headers: {
209 | "Access-Control-Allow-Origin": "*"
210 | }
211 | });
212 | }
213 | });
214 | } else if (action == "rebuild") {
215 | // 'rebuild' handles rebuilding the WorkSpace assigned to the user that submits the API call.
216 | // A rebuild function resets the WorkSpace back to its original state. Applications or system settings changes
217 | // will be lost during a rebuild. The Data Drive is recreated from the last snapshot; snapshots are taken every 12 hours.
218 |
219 | console.log(
220 | "Trying to find desktop owned by: " +
221 | event.requestContext.authorizer.claims.email
222 | );
223 |
224 | var describeWorkspacesParams = [];
225 |
226 | workspaces.describeWorkspaces(describeWorkspacesParams, function (
227 | err,
228 | data
229 | ) {
230 | if (err) {
231 | console.log(err, err.stack); // an error occurred
232 | } else {
233 | for (var i = 0; i < data.Workspaces.length; i++) {
234 | var describeTagsParams = {
235 | ResourceId: data.Workspaces[i].WorkspaceId /* required */
236 | };
237 | workspaces.describeTags(describeTagsParams, function (err, data) {
238 | if (err) {
239 | console.log(err, err.stack);
240 | } else {
241 | for (var i = 0; i < data.TagList.length; i++) {
242 | if (
243 | data.TagList[i].Key == "SelfServiceManaged" &&
244 | data.TagList[i].Value ==
245 | event.requestContext.authorizer.claims.email
246 | ) {
247 | console.log(
248 | "Desktop for '" +
249 | event.requestContext.authorizer.claims.email +
250 | "' found: " +
251 | describeTagsParams.ResourceId
252 | );
253 | console.log(
254 | "Rebuilding desktop '" +
255 | describeTagsParams.ResourceId +
256 | " per request."
257 | );
258 |
259 | var rebuildParams = {
260 | RebuildWorkspaceRequests: [{
261 | WorkspaceId: describeTagsParams.ResourceId
262 | }]
263 | };
264 |
265 | console.log(JSON.stringify(rebuildParams));
266 |
267 | workspaces.rebuildWorkspaces(rebuildParams, function (
268 | err,
269 | data
270 | ) {
271 | if (err) {
272 | console.log("Error: " + err);
273 | callback(null, {
274 | statusCode: 500,
275 | body: JSON.stringify({
276 | Error: err
277 | }),
278 | headers: {
279 | "Access-Control-Allow-Origin": "*"
280 | }
281 | });
282 | } else {
283 | console.log("Result: " + JSON.stringify(data));
284 |
285 | callback(null, {
286 | statusCode: 200,
287 | body: JSON.stringify({
288 | Result: data
289 | }),
290 | headers: {
291 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
292 | "Access-Control-Allow-Methods": "GET,OPTIONS",
293 | "Access-Control-Allow-Origin": originURL
294 | }
295 | });
296 | }
297 | });
298 | }
299 | }
300 | }
301 | });
302 | }
303 | }
304 | });
305 | } else if (action == "reboot") {
306 | // 'rebuild' handles rebooting the WorkSpace assigned to the user that submits the API call.
307 |
308 | console.log(
309 | "Trying to find desktop owned by: " +
310 | event.requestContext.authorizer.claims.email
311 | );
312 |
313 | var describeWorkspacesParams = [];
314 |
315 | workspaces.describeWorkspaces(describeWorkspacesParams, function (
316 | err,
317 | data
318 | ) {
319 | if (err) {
320 | console.log(err, err.stack); // an error occurred
321 | } else {
322 | for (var i = 0; i < data.Workspaces.length; i++) {
323 | var describeTagsParams = {
324 | ResourceId: data.Workspaces[i].WorkspaceId /* required */
325 | };
326 | workspaces.describeTags(describeTagsParams, function (err, data) {
327 | if (err) {
328 | console.log(err, err.stack);
329 | } else {
330 | for (var i = 0; i < data.TagList.length; i++) {
331 | if (
332 | data.TagList[i].Key == "SelfServiceManaged" &&
333 | data.TagList[i].Value ==
334 | event.requestContext.authorizer.claims.email
335 | ) {
336 | console.log(
337 | "Desktop for '" +
338 | event.requestContext.authorizer.claims.email +
339 | "' found: " +
340 | describeTagsParams.ResourceId
341 | );
342 | console.log(
343 | "Rebooting desktop '" +
344 | describeTagsParams.ResourceId +
345 | " per request."
346 | );
347 |
348 | var rebootParams = {
349 | RebootWorkspaceRequests: [{
350 | WorkspaceId: describeTagsParams.ResourceId
351 | }]
352 | };
353 |
354 | console.log(JSON.stringify(rebootParams));
355 |
356 | workspaces.rebootWorkspaces(rebootParams, function (
357 | err,
358 | data
359 | ) {
360 | if (err) {
361 | console.log("Error: " + err);
362 | callback(null, {
363 | statusCode: 500,
364 | body: JSON.stringify({
365 | Error: err
366 | }),
367 | headers: {
368 | "Access-Control-Allow-Origin": "*"
369 | }
370 | });
371 | } else {
372 | console.log("Result: " + JSON.stringify(data));
373 |
374 | callback(null, {
375 | statusCode: 200,
376 | body: JSON.stringify({
377 | Result: data
378 | }),
379 | headers: {
380 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
381 | "Access-Control-Allow-Methods": "GET,OPTIONS",
382 | "Access-Control-Allow-Origin": originURL
383 | }
384 | });
385 | }
386 | });
387 | }
388 | }
389 | }
390 | });
391 | }
392 | }
393 | });
394 | } else if (action == "delete") {
395 | // 'delete' handles deleting the WorkSpace assigned to the user that submits the API call.
396 | // This is a permanent action and cannot be undone. No data will persist after removal.
397 |
398 | console.log(
399 | "Trying to find desktop owned by: " +
400 | event.requestContext.authorizer.claims.email
401 | );
402 |
403 | var describeWorkspacesParams = [];
404 |
405 | workspaces.describeWorkspaces(describeWorkspacesParams, function (
406 | err,
407 | data
408 | ) {
409 | if (err) {
410 | console.log(err, err.stack); // an error occurred
411 | } else {
412 | for (var i = 0; i < data.Workspaces.length; i++) {
413 | var describeTagsParams = {
414 | ResourceId: data.Workspaces[i].WorkspaceId /* required */
415 | };
416 | workspaces.describeTags(describeTagsParams, function (err, data) {
417 | if (err) {
418 | console.log(err, err.stack);
419 | } else {
420 | for (var i = 0; i < data.TagList.length; i++) {
421 | if (
422 | data.TagList[i].Key == "SelfServiceManaged" &&
423 | data.TagList[i].Value ==
424 | event.requestContext.authorizer.claims.email
425 | ) {
426 | console.log(
427 | "Desktop for '" +
428 | event.requestContext.authorizer.claims.email +
429 | "' found: " +
430 | describeTagsParams.ResourceId
431 | );
432 | console.log(
433 | "Deleting desktop '" +
434 | describeTagsParams.ResourceId +
435 | " per request."
436 | );
437 |
438 | var deletionParams = {
439 | TerminateWorkspaceRequests: [{
440 | WorkspaceId: describeTagsParams.ResourceId
441 | }]
442 | };
443 |
444 | console.log(JSON.stringify(deletionParams));
445 |
446 | workspaces.terminateWorkspaces(deletionParams, function (
447 | err,
448 | data
449 | ) {
450 | if (err) {
451 | console.log("Error: " + err);
452 | callback(null, {
453 | statusCode: 500,
454 | body: JSON.stringify({
455 | Error: err
456 | }),
457 | headers: {
458 | "Access-Control-Allow-Origin": "*"
459 | }
460 | });
461 | } else {
462 | console.log("Result: " + JSON.stringify(data));
463 |
464 | callback(null, {
465 | statusCode: 200,
466 | body: JSON.stringify({
467 | Result: data
468 | }),
469 | headers: {
470 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
471 | "Access-Control-Allow-Methods": "GET,OPTIONS",
472 | "Access-Control-Allow-Origin": originURL
473 | }
474 | });
475 | }
476 | });
477 | }
478 | }
479 | }
480 | });
481 | }
482 | }
483 | });
484 | } else if (action == "bundles") {
485 | // 'bundles' handles returning the list of WorkSpaces bundles available to use. The WorkSpaces API only returns a page at a time, so we must recursively
486 | // make the call to describeWorkspaceBundles until NextToken is null.
487 | // We must make the API call twice to return bundles owned by AMAZON and custom bundles.
488 |
489 | var bundleList = [];
490 |
491 | function getBundles(parameters, cb) {
492 | workspaces.describeWorkspaceBundles(parameters, function (err, data) {
493 | if (err) {
494 | callback(null, {
495 | statusCode: 500,
496 | body: JSON.stringify({
497 | Error: err
498 | }),
499 | headers: {
500 | "Access-Control-Allow-Origin": "*"
501 | }
502 | });
503 | } else {
504 | for (var i = 0; i < data["Bundles"].length; i++) {
505 | console.log(
506 | data["Bundles"][i].BundleId + ":" + data["Bundles"][i].Name
507 | );
508 | bundleList.push(
509 | data["Bundles"][i].BundleId + ":" + data["Bundles"][i].Name
510 | );
511 | }
512 | console.log("NextToken: " + data["NextToken"]);
513 |
514 | if (data.NextToken) {
515 | parameters.NextToken = data["NextToken"];
516 | getBundles(parameters, cb);
517 | } else {
518 | cb();
519 | }
520 | }
521 | });
522 | }
523 |
524 | // Get the Amazon-owned bundle list first, and then get the Customer-owned bundles next, and then return the entire list.
525 | getBundles({
526 | Owner: "AMAZON"
527 | },
528 | function () {
529 | getBundles({
530 | Owner: null
531 | },
532 | function () {
533 | callback(null, {
534 | statusCode: 200,
535 | body: JSON.stringify({
536 | Result: bundleList
537 | }),
538 | headers: {
539 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
540 | "Access-Control-Allow-Methods": "GET,OPTIONS",
541 | "Access-Control-Allow-Origin": originURL
542 | }
543 | });
544 | }
545 | );
546 | }
547 | );
548 | } else {
549 | console.log("No action specified.");
550 | callback(null, {
551 | statusCode: 500,
552 | body: JSON.stringify({
553 | Error: "No action specified."
554 | }),
555 | headers: {
556 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
557 | "Access-Control-Allow-Methods": "GET,OPTIONS",
558 | "Access-Control-Allow-Origin": originURL
559 | }
560 | });
561 | }
562 | };
--------------------------------------------------------------------------------
/deploy.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "WorkSpaces Portal Foundational Services and Pipeline",
4 | "Parameters": {
5 | "AppName": {
6 | "Type": "String",
7 | "Description": "Name of the application.",
8 | "MinLength": "1",
9 | "MaxLength": "80",
10 | "AllowedPattern": "[A-Za-z0-9-]+",
11 | "ConstraintDescription": "Malformed input parameter. AppName must only contain upper and lower case letters, numbers, and -."
12 | },
13 | "BucketName": {
14 | "Description": "Name of the new S3 Bucket to Host Site",
15 | "Type": "String",
16 | "AllowedPattern": "[a-z0-9-]+",
17 | "Default": "eeg3-serverless-template"
18 | },
19 | "CognitoPool": {
20 | "Description": "Name for the Cognito Pool",
21 | "Type": "String",
22 | "Default": "serverless-pool"
23 | },
24 | "SAMInputFile": {
25 | "Type": "String",
26 | "Description": "The filename for the serverless transform file.",
27 | "Default": "wsportal.json"
28 | },
29 | "SAMOutputFile": {
30 | "Type": "String",
31 | "Description": "The filename for the output file from the buildspec file.",
32 | "Default": "post-package.yaml"
33 | },
34 | "CodeBuildImage": {
35 | "Type": "String",
36 | "Default": "node:latest",
37 | "Description": "Image used for CodeBuild project."
38 | },
39 | "GitHubRepoName": {
40 | "Type": "String",
41 | "Description": "The GitHub repo name"
42 | },
43 | "GitHubRepoBranch": {
44 | "Type": "String",
45 | "Description": "The GitHub repo branch code pipelines should watch for changes on",
46 | "Default": "master"
47 | },
48 | "GitHubUser": {
49 | "Type": "String",
50 | "Description": "GitHub UserName. This username must have access to the GitHubToken."
51 | },
52 | "GitHubToken": {
53 | "NoEcho": true,
54 | "Type": "String",
55 | "Description": "Secret. OAuthToken with access to Repo. Long string of characters and digits. Go to https://github.com/settings/tokens"
56 | }
57 | },
58 | "Resources": {
59 | "S3Bucket": {
60 | "Type": "AWS::S3::Bucket",
61 | "Properties": {
62 | "AccessControl": "PublicRead",
63 | "BucketName": {
64 | "Ref": "BucketName"
65 | },
66 | "WebsiteConfiguration": {
67 | "IndexDocument": "index.html"
68 | }
69 | },
70 | "DeletionPolicy": "Retain"
71 | },
72 | "S3WebsiteBucketPolicy": {
73 | "DependsOn": [
74 | "S3Bucket"
75 | ],
76 | "Description": "Setting Amazon S3 bucket policy for external and AWS CodePipeline access",
77 | "Type": "AWS::S3::BucketPolicy",
78 | "Properties": {
79 | "Bucket": {
80 | "Ref": "S3Bucket"
81 | },
82 | "PolicyDocument": {
83 | "Version": "2012-10-17",
84 | "Id": "SSEAndSSLPolicy",
85 | "Statement": [{
86 | "Action": [
87 | "s3:GetObject"
88 | ],
89 | "Effect": "Allow",
90 | "Resource": {
91 | "Fn::Sub": "arn:aws:s3:::${S3Bucket}/*"
92 | },
93 | "Principal": "*"
94 | }]
95 | }
96 | }
97 | },
98 | "CodeBuildTrustRole": {
99 | "Description": "Creating service role in IAM for AWS CodeBuild",
100 | "Type": "AWS::IAM::Role",
101 | "Properties": {
102 | "RoleName": {
103 | "Fn::Sub": "${AppName}-codebuild-role"
104 | },
105 | "AssumeRolePolicyDocument": {
106 | "Statement": [{
107 | "Effect": "Allow",
108 | "Principal": {
109 | "Service": [
110 | "codebuild.amazonaws.com"
111 | ]
112 | },
113 | "Action": "sts:AssumeRole"
114 | }]
115 | },
116 | "Path": "/"
117 | }
118 | },
119 | "CodeBuildRolePolicy": {
120 | "Type": "AWS::IAM::Policy",
121 | "DependsOn": "CodeBuildTrustRole",
122 | "Description": "Setting IAM policy for the service role for AWS CodeBuild",
123 | "Properties": {
124 | "PolicyName": "CodeBuildRolePolicy",
125 | "PolicyDocument": {
126 | "Statement": [{
127 | "Effect": "Allow",
128 | "Action": [
129 | "logs:CreateLogGroup",
130 | "logs:CreateLogStream",
131 | "logs:PutLogEvents"
132 | ],
133 | "Resource": [
134 | "*"
135 | ]
136 | },
137 | {
138 | "Effect": "Allow",
139 | "Resource": [
140 | "*"
141 | ],
142 | "Action": [
143 | "s3:*"
144 | ]
145 | },
146 | {
147 | "Effect": "Allow",
148 | "Resource": [
149 | "*"
150 | ],
151 | "Action": [
152 | "kms:GenerateDataKey*",
153 | "kms:Encrypt",
154 | "kms:Decrypt"
155 | ]
156 | },
157 | {
158 | "Effect": "Allow",
159 | "Resource": [
160 | "*"
161 | ],
162 | "Action": [
163 | "sns:SendMessage"
164 | ]
165 | }
166 | ]
167 | },
168 | "Roles": [{
169 | "Ref": "CodeBuildTrustRole"
170 | }]
171 | }
172 | },
173 | "CloudFormationTrustRole": {
174 | "Description": "Creating service role in IAM for AWS CloudFormation",
175 | "Type": "AWS::IAM::Role",
176 | "Properties": {
177 | "RoleName": {
178 | "Fn::Sub": "${AppName}-cloudformation-role"
179 | },
180 | "AssumeRolePolicyDocument": {
181 | "Statement": [{
182 | "Effect": "Allow",
183 | "Principal": {
184 | "Service": [
185 | "cloudformation.amazonaws.com"
186 | ]
187 | },
188 | "Action": "sts:AssumeRole"
189 | }]
190 | },
191 | "Path": "/"
192 | }
193 | },
194 | "CloudFormationRolePolicy": {
195 | "Type": "AWS::IAM::Policy",
196 | "DependsOn": "CloudFormationTrustRole",
197 | "Description": "Setting IAM policy for the service role for AWS CloudFormation",
198 | "Properties": {
199 | "PolicyName": "CloudFormationRolePolicy",
200 | "PolicyDocument": {
201 | "Statement": [{
202 | "Action": [
203 | "s3:GetObject",
204 | "s3:GetObjectVersion",
205 | "s3:GetBucketVersioning"
206 | ],
207 | "Resource": "*",
208 | "Effect": "Allow"
209 | },
210 | {
211 | "Action": [
212 | "s3:PutObject"
213 | ],
214 | "Resource": [
215 | "arn:aws:s3:::codepipeline*"
216 | ],
217 | "Effect": "Allow"
218 | },
219 | {
220 | "Action": [
221 | "lambda:*"
222 | ],
223 | "Resource": {
224 | "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:*"
225 | },
226 | "Effect": "Allow"
227 | },
228 | {
229 | "Action": [
230 | "lambda:ListTags",
231 | "lambda:TagResource",
232 | "lambda:UntagResource"
233 | ],
234 | "Resource": "*",
235 | "Effect": "Allow"
236 | },
237 | {
238 | "Action": [
239 | "apigateway:*"
240 | ],
241 | "Resource": {
242 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}::*"
243 | },
244 | "Effect": "Allow"
245 | },
246 | {
247 | "Action": [
248 | "iam:GetRole",
249 | "iam:CreateRole",
250 | "iam:DeleteRole",
251 | "iam:AttachRolePolicy",
252 | "iam:DetachRolePolicy",
253 | "iam:PutRolePolicy",
254 | "iam:DeleteRolePolicy",
255 | "iam:UpdateAssumeRolePolicy"
256 | ],
257 | "Resource": {
258 | "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/*"
259 | },
260 | "Effect": "Allow"
261 | },
262 | {
263 | "Action": [
264 | "states:*"
265 | ],
266 | "Resource": {
267 | "Fn::Sub": "arn:aws:states:${AWS::Region}:${AWS::AccountId}:*"
268 | },
269 | "Effect": "Allow"
270 | },
271 | {
272 | "Action": [
273 | "iam:AttachRolePolicy",
274 | "iam:DetachRolePolicy"
275 | ],
276 | "Resource": {
277 | "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/${AppName}-*"
278 | },
279 | "Effect": "Allow"
280 | },
281 | {
282 | "Action": [
283 | "iam:PassRole"
284 | ],
285 | "Resource": [
286 | "*"
287 | ],
288 | "Effect": "Allow"
289 | },
290 | {
291 | "Action": [
292 | "events:*"
293 | ],
294 | "Resource": [
295 | "*"
296 | ],
297 | "Effect": "Allow"
298 | },
299 | {
300 | "Action": [
301 | "dynamodb:*"
302 | ],
303 | "Resource": [
304 | "*"
305 | ],
306 | "Effect": "Allow"
307 | },
308 | {
309 | "Action": [
310 | "cloudformation:CreateChangeSet"
311 | ],
312 | "Resource": {
313 | "Fn::Sub": "arn:aws:cloudformation:${AWS::Region}:aws:transform/Serverless-2016-10-31"
314 | },
315 | "Effect": "Allow"
316 | }
317 | ]
318 | },
319 | "Roles": [{
320 | "Ref": "CloudFormationTrustRole"
321 | }]
322 | }
323 | },
324 | "CodePipelineTrustRole": {
325 | "Description": "Creating service role in IAM for AWS CodePipeline",
326 | "Type": "AWS::IAM::Role",
327 | "Properties": {
328 | "RoleName": {
329 | "Fn::Sub": "${AppName}-codepipeline-role"
330 | },
331 | "AssumeRolePolicyDocument": {
332 | "Statement": [{
333 | "Effect": "Allow",
334 | "Principal": {
335 | "Service": [
336 | "codepipeline.amazonaws.com"
337 | ]
338 | },
339 | "Action": "sts:AssumeRole"
340 | }]
341 | },
342 | "Path": "/"
343 | }
344 | },
345 | "CodePipelineRolePolicy": {
346 | "Type": "AWS::IAM::Policy",
347 | "DependsOn": "CodePipelineTrustRole",
348 | "Description": "Setting IAM policy for the service role for AWS CodePipeline",
349 | "Properties": {
350 | "PolicyName": "CodePipelineRolePolicy",
351 | "PolicyDocument": {
352 | "Statement": [{
353 | "Action": [
354 | "s3:GetObject",
355 | "s3:GetObjectVersion",
356 | "s3:GetBucketVersioning"
357 | ],
358 | "Resource": "*",
359 | "Effect": "Allow"
360 | },
361 | {
362 | "Action": [
363 | "s3:PutObject"
364 | ],
365 | "Resource": [
366 | "arn:aws:s3:::codepipeline*"
367 | ],
368 | "Effect": "Allow"
369 | },
370 | {
371 | "Action": [
372 | "codebuild:StartBuild",
373 | "codebuild:BatchGetBuilds"
374 | ],
375 | "Resource": "*",
376 | "Effect": "Allow"
377 | },
378 | {
379 | "Action": [
380 | "cloudwatch:*",
381 | "s3:*",
382 | "sns:*",
383 | "cloudformation:*",
384 | "rds:*",
385 | "sqs:*",
386 | "iam:PassRole"
387 | ],
388 | "Resource": "*",
389 | "Effect": "Allow"
390 | },
391 | {
392 | "Action": [
393 | "lambda:InvokeFunction",
394 | "lambda:ListFunctions"
395 | ],
396 | "Resource": "*",
397 | "Effect": "Allow"
398 | }
399 | ]
400 | },
401 | "Roles": [{
402 | "Ref": "CodePipelineTrustRole"
403 | }]
404 | }
405 | },
406 | "CodeBuildProject": {
407 | "DependsOn": [
408 | "S3ArtifactsBucket",
409 | "CodeBuildTrustRole"
410 | ],
411 | "Description": "Creating AWS CodeBuild project",
412 | "Type": "AWS::CodeBuild::Project",
413 | "Properties": {
414 | "Artifacts": {
415 | "Type": "CODEPIPELINE"
416 | },
417 | "Description": {
418 | "Fn::Sub": "Building stage for ${AppName}."
419 | },
420 | "Environment": {
421 | "ComputeType": "BUILD_GENERAL1_SMALL",
422 | "EnvironmentVariables": [{
423 | "Name": "S3_ARTIFACT_BUCKET",
424 | "Value": {
425 | "Ref": "S3ArtifactsBucket"
426 | }
427 | },
428 | {
429 | "Name": "S3_WEBSITE_BUCKET",
430 | "Value": {
431 | "Ref": "S3Bucket"
432 | }
433 | },
434 | {
435 | "Name": "INPUT_FILE",
436 | "Value": {
437 | "Ref": "SAMInputFile"
438 | }
439 | }
440 | ],
441 | "Image": {
442 | "Ref": "CodeBuildImage"
443 | },
444 | "Type": "LINUX_CONTAINER"
445 | },
446 | "Name": {
447 | "Fn::Sub": "${AppName}-build"
448 | },
449 | "ServiceRole": {
450 | "Fn::GetAtt": [
451 | "CodeBuildTrustRole",
452 | "Arn"
453 | ]
454 | },
455 | "Source": {
456 | "Type": "CODEPIPELINE"
457 | },
458 | "Tags": [{
459 | "Key": "app-name",
460 | "Value": {
461 | "Ref": "AppName"
462 | }
463 | }],
464 | "TimeoutInMinutes": 5
465 | }
466 | },
467 | "S3ArtifactsBucket": {
468 | "Description": "Creating S3 bucket for AWS CodePipeline artifacts",
469 | "Type": "AWS::S3::Bucket",
470 | "DeletionPolicy": "Retain",
471 | "Properties": {
472 | "BucketName": {
473 | "Fn::Sub": "serverless-app-${AWS::AccountId}-${AWS::Region}-${AppName}"
474 | },
475 | "VersioningConfiguration": {
476 | "Status": "Enabled"
477 | }
478 | }
479 | },
480 | "S3ArtifactBucketPolicy": {
481 | "DependsOn": [
482 | "S3ArtifactsBucket"
483 | ],
484 | "Description": "Setting Amazon S3 bucket policy for AWS CodePipeline access",
485 | "Type": "AWS::S3::BucketPolicy",
486 | "Properties": {
487 | "Bucket": {
488 | "Ref": "S3ArtifactsBucket"
489 | },
490 | "PolicyDocument": {
491 | "Version": "2012-10-17",
492 | "Id": "SSEAndSSLPolicy",
493 | "Statement": [{
494 | "Sid": "DenyInsecureConnections",
495 | "Effect": "Deny",
496 | "Principal": "*",
497 | "Action": "s3:*",
498 | "Resource": {
499 | "Fn::Sub": "arn:aws:s3:::${S3ArtifactsBucket}/*"
500 | },
501 | "Condition": {
502 | "Bool": {
503 | "aws:SecureTransport": false
504 | }
505 | }
506 | }]
507 | }
508 | }
509 | },
510 | "ProjectPipeline": {
511 | "DependsOn": [
512 | "S3ArtifactsBucket",
513 | "CodeBuildProject",
514 | "CodePipelineTrustRole",
515 | "CloudFormationTrustRole"
516 | ],
517 | "Description": "Creating a deployment pipeline for your project in AWS CodePipeline",
518 | "Type": "AWS::CodePipeline::Pipeline",
519 | "Properties": {
520 | "Name": {
521 | "Fn::Sub": "${AppName}-pipeline"
522 | },
523 | "RoleArn": {
524 | "Fn::GetAtt": [
525 | "CodePipelineTrustRole",
526 | "Arn"
527 | ]
528 | },
529 | "Stages": [{
530 | "Name": "Source",
531 | "Actions": [{
532 | "Name": "source",
533 | "InputArtifacts": [],
534 | "ActionTypeId": {
535 | "Version": "1",
536 | "Category": "Source",
537 | "Owner": "ThirdParty",
538 | "Provider": "GitHub"
539 | },
540 | "OutputArtifacts": [{
541 | "Name": {
542 | "Fn::Sub": "${AppName}-SourceArtifact"
543 | }
544 | }],
545 | "Configuration": {
546 | "Repo": {
547 | "Ref": "GitHubRepoName"
548 | },
549 | "Branch": {
550 | "Ref": "GitHubRepoBranch"
551 | },
552 | "OAuthToken": {
553 | "Ref": "GitHubToken"
554 | },
555 | "Owner": {
556 | "Ref": "GitHubUser"
557 | }
558 | },
559 | "RunOrder": 1
560 | }]
561 | },
562 | {
563 | "Name": "Build",
564 | "Actions": [{
565 | "Name": "build-from-source",
566 | "InputArtifacts": [{
567 | "Name": {
568 | "Fn::Sub": "${AppName}-SourceArtifact"
569 | }
570 | }],
571 | "ActionTypeId": {
572 | "Category": "Build",
573 | "Owner": "AWS",
574 | "Version": "1",
575 | "Provider": "CodeBuild"
576 | },
577 | "OutputArtifacts": [{
578 | "Name": {
579 | "Fn::Sub": "${AppName}-BuildArtifact"
580 | }
581 | }],
582 | "Configuration": {
583 | "ProjectName": {
584 | "Fn::Sub": "${AppName}-build"
585 | }
586 | },
587 | "RunOrder": 1
588 | }]
589 | },
590 | {
591 | "Name": "Deploy",
592 | "Actions": [{
593 | "Name": "create-changeset",
594 | "InputArtifacts": [{
595 | "Name": {
596 | "Fn::Sub": "${AppName}-BuildArtifact"
597 | }
598 | }],
599 | "ActionTypeId": {
600 | "Category": "Deploy",
601 | "Owner": "AWS",
602 | "Version": "1",
603 | "Provider": "CloudFormation"
604 | },
605 | "OutputArtifacts": [],
606 | "Configuration": {
607 | "StackName": {
608 | "Fn::Sub": "${AppName}-serverless-stack"
609 | },
610 | "ActionMode": "CHANGE_SET_REPLACE",
611 | "RoleArn": {
612 | "Fn::GetAtt": [
613 | "CloudFormationTrustRole",
614 | "Arn"
615 | ]
616 | },
617 | "ChangeSetName": "pipeline-changeset",
618 | "Capabilities": "CAPABILITY_NAMED_IAM",
619 | "TemplatePath": {
620 | "Fn::Sub": "${AppName}-BuildArtifact::${SAMOutputFile}"
621 | }
622 | },
623 | "RunOrder": 1
624 | },
625 | {
626 | "Name": "execute-changeset",
627 | "InputArtifacts": [],
628 | "ActionTypeId": {
629 | "Category": "Deploy",
630 | "Owner": "AWS",
631 | "Version": "1",
632 | "Provider": "CloudFormation"
633 | },
634 | "OutputArtifacts": [],
635 | "Configuration": {
636 | "StackName": {
637 | "Fn::Sub": "${AppName}-serverless-stack"
638 | },
639 | "ActionMode": "CHANGE_SET_EXECUTE",
640 | "ChangeSetName": "pipeline-changeset"
641 | },
642 | "RunOrder": 2
643 | }
644 | ]
645 | }
646 | ],
647 | "ArtifactStore": {
648 | "Type": "S3",
649 | "Location": {
650 | "Ref": "S3ArtifactsBucket"
651 | }
652 | }
653 | }
654 | },
655 | "UserPool": {
656 | "Type": "AWS::Cognito::UserPool",
657 | "Properties": {
658 | "UserPoolName": {
659 | "Ref": "CognitoPool"
660 | },
661 | "AutoVerifiedAttributes": [
662 | "email"
663 | ],
664 | "EmailVerificationMessage": "Your verification code is {####}.",
665 | "EmailVerificationSubject": "Your verification code"
666 | }
667 | },
668 | "UserPoolClient": {
669 | "Type": "AWS::Cognito::UserPoolClient",
670 | "Properties": {
671 | "ClientName": "WebApp",
672 | "GenerateSecret": "false",
673 | "UserPoolId": {
674 | "Ref": "UserPool"
675 | }
676 | }
677 | },
678 | "CognitoConfigRole": {
679 | "Type": "AWS::IAM::Role",
680 | "Properties": {
681 | "Path": "/cognito/",
682 | "AssumeRolePolicyDocument": {
683 | "Version": "2012-10-17",
684 | "Statement": [{
685 | "Effect": "Allow",
686 | "Principal": {
687 | "Service": "lambda.amazonaws.com"
688 | },
689 | "Action": "sts:AssumeRole"
690 | }]
691 | },
692 | "Policies": [{
693 | "PolicyName": "CognitoConfig",
694 | "PolicyDocument": {
695 | "Version": "2012-10-17",
696 | "Statement": [{
697 | "Sid": "Logging",
698 | "Effect": "Allow",
699 | "Action": [
700 | "logs:CreateLogGroup",
701 | "logs:CreateLogStream",
702 | "logs:PutLogEvents"
703 | ],
704 | "Resource": "*"
705 | },
706 | {
707 | "Sid": "Cognito",
708 | "Effect": "Allow",
709 | "Action": [
710 | "cognito-idp:CreateUserPool",
711 | "cognito-idp:DeleteUserPool",
712 | "cognito-idp:CreateUserPoolClient",
713 | "cognito-idp:DeleteUserPoolClient"
714 | ],
715 | "Resource": "*"
716 | },
717 | {
718 | "Sid": "ConfigBucketWriteAccess",
719 | "Effect": "Allow",
720 | "Action": [
721 | "s3:PutObject",
722 | "s3:PutObjectAcl",
723 | "s3:PutObjectVersionAcl"
724 | ],
725 | "Resource": [{
726 | "Fn::Sub": "arn:aws:s3:::${BucketName}/*"
727 | }]
728 | }
729 | ]
730 | }
731 | }]
732 | }
733 | }
734 | },
735 | "Outputs": {
736 | "BucketName": {
737 | "Value": {
738 | "Ref": "S3Bucket"
739 | },
740 | "Description": "The created bucket name"
741 | },
742 | "OriginURL": {
743 | "Value": {
744 | "Fn::GetAtt": [
745 | "S3Bucket",
746 | "WebsiteURL"
747 | ]
748 | },
749 | "Description": "URL for the website hosted on S3"
750 | },
751 | "UserPoolId": {
752 | "Value": {
753 | "Ref": "UserPool"
754 | },
755 | "Description": "User Pool ID"
756 | },
757 | "UserPoolClientId": {
758 | "Value": {
759 | "Ref": "UserPoolClient"
760 | },
761 | "Description": "User Pool Client ID"
762 | }
763 | }
764 | }
--------------------------------------------------------------------------------