diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..c1a403648c --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +**/.class +**/.DS_Store +**/node_modules +**/npm-debug.log +**/pnpm-debug.log + +.build + +# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/*.example.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +# IntelliJ Idea Module Files +*.iml +**/.idea + +# User-config files +main/config/settings/*.yml +main/solution/post-deployment/config/settings/*.yml +main/solution/prepare-master-acc/config/settings/*.yml +main/cicd/cicd-pipeline/config/settings/*.yml +main/cicd/cicd-source/config/settings/*.yml \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..6a9bbf8ea4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,53 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch sls offline", + "program": "${workspaceFolder}/main/solution/backend/node_modules/serverless/bin/serverless", + "cwd": "${workspaceFolder}/main/solution/backend", + "args": [ + "offline", + "-s", + "${env:USER}", + "--noTimeout", + "--dontPrintOutput" + ], + "outFiles": ["${workspaceFolder}/main/solution/backend/.webpack/**/*.js"], + "sourceMaps": true, + "protocol": "inspector", + "runtimeExecutable": "node", + "runtimeArgs": ["--lazy"] + }, + { + "type": "node", + "request": "launch", + "name": "Launch sls post deployment", + "program": "${workspaceFolder}/main/solution/backend/node_modules/serverless/bin/serverless", + "cwd": "${workspaceFolder}/main/solution/post-deployment", + "args": [ + "invoke", + "local", + "-f", + "postDeployment", + "-s", + "${env:USER}", + "--noTimeout", + "--dontPrintOutput", + "--data", + "{}" + ], + "outFiles": [ + "${workspaceFolder}/main/solution/post-deployment/.webpack/**/*.js" + ], + "sourceMaps": true, + "protocol": "inspector", + "runtimeExecutable": "node", + "runtimeArgs": ["--lazy"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..0532f4db2a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "cSpell.words": [ + "Cognito", + "Dropdown", + "Errored", + "Overrider", + "autorun", + "basedl", + "cloudfront", + "lifecycle", + "mobx", + "overridables", + "raas", + "sagemaker", + "skippable", + "stackable", + "webapp" + ], + "editor.formatOnSave": true, + "eslint.packageManager": "pnpm", + "prettier.packageManager": "pnpm", + "eslint.workingDirectories": [{ "mode": "auto" }] +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..33f3f11bf0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @awslabs/aws-go-research \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..5b627cfa60 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..914e0741d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..67db858821 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/LICENSE-LAMBDA b/LICENSE-LAMBDA new file mode 100644 index 0000000000..6aa0c458f3 --- /dev/null +++ b/LICENSE-LAMBDA @@ -0,0 +1,14 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE-SUMMARY b/LICENSE-SUMMARY new file mode 100644 index 0000000000..578dbab990 --- /dev/null +++ b/LICENSE-SUMMARY @@ -0,0 +1,2 @@ +This project is licensed under the terms of the Apache 2.0 license. See LICENSE. +Included AWS Lambda functions are licensed under the MIT-0 license. See LICENSE-LAMBDA. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..616fc58894 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..dbd8255987 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# AWS Galileo Gateway + +A platform that provides researchers with one-click access to collaborative workspace environments operating across teams, universities, and datasets while enabling university IT stakeholders to manage, monitor, and control spending, apply security best practices, and comply with corporate governance. + +Platform provides one-click option to admins for easier creation (vending) of new AWS accounts specific to researchers' teams for easier governance. + +For more information about various AWS accounts see [aws-accounts-readme.md](main/documentation/aws-accounts-readme.md). + +The solution contains the following components: + +- solution/infrastructure/ +- solution/backend/ +- solution/edge-lambda +- solution/machine-images/ +- solution/prepare-master-acc +- solution/post-deployment/ +- solution/ui/ + +The solution also includes a Continuous Integration/Continuous Delivery feature: + +- main/cicd/cicd-pipeline +- main/cicd/cicd-source + +--- + +## Getting Started + +Node.js v12.x or later is required. + +Before you can build this project, you need to install [pnpm](https://pnpm.js.org/en/). Run the following command: + +```bash +$ npm install -g pnpm +``` + +To create the initial settings files, take a look at the example.yaml settings file in main/config/example.yaml and create your own copy. +The stage is either 'example' or your username. This method should be used only for the very first time you install this solution. +In the rest of this README, \$STAGE is used to designate the stage. + +Now, let's perform an initial deployment: + +```bash +$ scripts/environment-deploy.sh +``` + +Following an initial successful deployment, you can subsequently deploy updates to the infrastructure, backend, and post-deployment components as follows: + +```bash +$ cd main/solution/ +$ pnpx sls deploy -s $STAGE +$ cd - +``` + +To run (rerun) the post-deployment steps: + +```bash +$ cd main/solution/post-deployment +$ pnpx sls invoke -f postDeployment -s $STAGE +$ cd - +``` + +To re-deploy the UI + +```bash +$ cd main/solution/ui +$ pnpx sls package-ui --stage $STAGE --local=true +$ pnpx sls package-ui --stage $STAGE +$ pnpx sls deploy-ui --stage $STAGE --invalidate-cache=true +$ cd - +``` + +To view information about the deployed components (e.g. CloudFront URL, root password), run the +following, where `[stage]` is the name of the environment (defaults to `$STAGE` if not provided): + +```bash +scripts/get-info.sh [stage] +``` + +Once you have deployed the app and the UI, you can start developing locally on your computer. +You will be running a local server that uses the same lambda functions code. To start local development, run the following commands to run a local server: + +```bash +$ cd main/solution/backend +$ pnpx sls offline -s $STAGE +$ cd - +``` + +Then, in a separate terminal, run the following commands to start the ui server and open up a browser: + +```bash +$ cd main/solution/ui +$ pnpx sls start-ui -s $STAGE +$ cd - +``` + +--- + +## Audits + +To audit the installed NPM packages, run the following commands: + +```bash +$ cd +$ pnpm audit +``` + +Please follow prevailing best practices for auditing your NPM dependencies and fixing them as needed. + +--- + +## Recommended Reading + +- [Serverless Framework for AWS](https://serverless.com/framework/docs/providers/aws/) +- [Serverless Stack](https://serverless-stack.com/) +- [Configure Multiple AWS Profiles](https://serverless-stack.com/chapters/configure-multiple-aws-profiles.html) +- [Serverless Offline](https://github.com/dherault/serverless-offline) + +## License + +This project is licensed under the terms of the Apache 2.0 license. See [LICENSE](LICENSE). +Included AWS Lambda functions are licensed under the MIT-0 license. See [LICENSE-LAMBDA](LICENSE-LAMBDA). diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt new file mode 100644 index 0000000000..62b007ff14 --- /dev/null +++ b/THIRD-PARTY-LICENSES.txt @@ -0,0 +1,130 @@ +** semantic-ui-css; version 2.4.2 -- https://semantic-ui.com/ +Copyright (c) 2015 Semantic Org + +The MIT License (MIT) + +Copyright (c) 2015 Semantic Org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** semantic-ui; version 2.4.2 -- https://github.com/Semantic-Org/Semantic-UI +Copyright (c) 2015 Semantic Org + +The MIT License +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** animate; version 3.7.0 -- http://daneden.me/animate +Copyright (c) 2018 Daniel Eden + +The MIT License (MIT) + +Copyright (c) 2018 Daniel Eden + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** basscss; version 8.0.4 -- https://github.com/basscss/basscss +Copyright (c) 2013 – 2016 Brent Jackson + +The MIT License (MIT) + +Copyright (c) 2013 – 2016 Brent Jackson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** semantic-ui-react; version 0.88.2 -- +https://github.com/Semantic-Org/Semantic-UI-React +Copyright (c) 2016 TechnologyAdvice + +MIT License + +Copyright (c) 2016 TechnologyAdvice + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json b/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json new file mode 100644 index 0000000000..a9e56eda24 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"], + "plugins": ["jest", "prettier"], + "rules": { + "prettier/prettier": ["error"], + "no-unused-vars": [ + "error", + { + "varsIgnorePattern": "^_.+", + "argsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "args": "after-used", + "ignoreRestSiblings": true + } + ], + "prefer-destructuring": 0, + "no-underscore-dangle": 0, + "no-param-reassign": 0, + "class-methods-use-this": 0, + "no-use-before-define": 0 + }, + "env": { + "jest/globals": true + } +} diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore b/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore new file mode 100644 index 0000000000..f15d856fe2 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore @@ -0,0 +1,22 @@ +**/.class +**/.DS_Store +**/node_modules + +**/npm-debug.log +**/pnpm-debug.log + +# Serverless directories +.serverless + +# Webpack generated directories +.webpack + +# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# IntelliJ Idea Module Files +*.iml diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json b/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json new file mode 100644 index 0000000000..a333103711 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "printWidth": 120, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "all" +} diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/README.md b/addons/addon-base-post-deployment/packages/base-post-deployment/README.md new file mode 100644 index 0000000000..644b0e6905 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/README.md @@ -0,0 +1,13 @@ +## To invoke post deployment locally + +After you run + +``` +$ pnpx sls deploy -s +``` + +You can invoke lambda locally + +``` +$ pnpx sls invoke local -f postDeployment -s +``` diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js b/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js new file mode 100644 index 0000000000..3f7ffc8068 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// jest.config.js +module.exports = { + // verbose: true, + notify: false, + testEnvironment: 'node', + // testPathIgnorePatterns: ['service.test.js'], + + // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports + // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html + reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]], +}; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json b/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json new file mode 100644 index 0000000000..780d3afae6 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js new file mode 100644 index 0000000000..0652f2e20a --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const Service = require('@aws-ee/base-services-container/lib/service'); +const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils'); + +const createOrUpdateSchema = require('./schema/deployment-item'); + +const settingKeys = { + tableName: 'dbTableDeploymentStore', +}; + +class DeploymentStoreService extends Service { + constructor() { + super(); + this.dependency(['jsonSchemaValidationService', 'dbService']); + } + + async init() { + await super.init(); + const [dbService] = await this.service(['dbService']); + const table = this.settings.get(settingKeys.tableName); + + this._getter = () => dbService.helper.getter().table(table); + this._updater = () => dbService.helper.updater().table(table); + this._query = () => dbService.helper.query().table(table); + this._deleter = () => dbService.helper.deleter().table(table); + } + + async find({ type, id, fields = [] }) { + return this._getter() + .key({ type, id }) + .projection(fields) + .get(); + } + + async mustFind({ type, id, fields = [] }) { + const result = await this.find({ type, id, fields }); + if (!result) throw this.boom.notFound(`deployment item of type "${type}" and id "${id}" does not exist`, true); + return result; + } + + async createOrUpdate(rawData) { + const [validationService] = await this.service(['jsonSchemaValidationService']); + + // Validate input + await validationService.ensureValid(rawData, createOrUpdateSchema); + + const { type, id } = rawData; + + // Time to save the the db object + return this._updater() + .key({ type, id }) + .item(rawData) + .update(); + } + + async delete({ type, id }) { + // Lets now remove the item from the database + const result = await runAndCatch( + async () => { + return this._deleter() + .condition('attribute_exists(type) and attribute_exists(id)') // yes we need this + .key({ type, id }) + .delete(); + }, + async () => { + throw this.boom.notFound(`deployment item of type "${type}" and id "${id}" does not exist`, true); + }, + ); + + return result; + } +} + +module.exports = DeploymentStoreService; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js new file mode 100644 index 0000000000..619b43e630 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js @@ -0,0 +1,107 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const AwsService = require('@aws-ee/base-services/lib/aws/aws-service'); +const S3Service = require('@aws-ee/base-services/lib/s3-service'); +const DbService = require('@aws-ee/base-services/lib/db-service'); +const JsonSchemaValidationService = require('@aws-ee/base-services/lib/json-schema-validation-service'); +const InputManifestValidationService = require('@aws-ee/base-services/lib/input-manifest/input-manifest-validation-service'); +const LockService = require('@aws-ee/base-services/lib/lock/lock-service'); +const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service'); +const AuditWriterService = require('@aws-ee/base-services/lib/audit/audit-writer-service'); +const AuthorizationService = require('@aws-ee/base-services/lib/authorization/authorization-service'); +const UserAuthzService = require('@aws-ee/base-services/lib/user/user-authz-service'); +const UserService = require('@aws-ee/base-services/lib/user/user-service'); +const DbPasswordService = require('@aws-ee/base-services/lib/db-password/db-password-service'); +const AuthenticationProviderTypeService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-type-service'); +const AuthenticationProviderConfigService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-config-service'); +const registerBuiltInAuthProvisioners = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provisioner-services.js'); + +const DeploymentStoreService = require('../deployment-store-service'); + +const settingKeys = { + tablePrefix: 'dbTablePrefix', +}; + +/** + * A function that registers base services required by the base addon for post-deployment lambda handler + * @param container An instance of ServicesContainer to register services to + * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point. + * + * @returns {Promise} + */ +// eslint-disable-next-line no-unused-vars +async function registerServices(container, pluginRegistry) { + container.register('aws', new AwsService(), { lazy: false }); + container.register('jsonSchemaValidationService', new JsonSchemaValidationService()); + container.register('authenticationProviderTypeService', new AuthenticationProviderTypeService()); + container.register('authenticationProviderConfigService', new AuthenticationProviderConfigService()); + container.register('lockService', new LockService()); + container.register('s3Service', new S3Service()); + container.register('dbService', new DbService(), { lazy: false }); + container.register('deploymentStoreService', new DeploymentStoreService()); + container.register('dbPasswordService', new DbPasswordService()); + container.register('userService', new UserService()); + container.register('inputManifestValidationService', new InputManifestValidationService()); + container.register('auditWriterService', new AuditWriterService()); + container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false }); + + // Authorization Services from base addon + container.register('authorizationService', new AuthorizationService()); + container.register('userAuthzService', new UserAuthzService()); + + registerBuiltInAuthProvisioners(container); +} + +/** + * A function that registers base static settings required by the base addon for api handler lambda function + * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins + * @param settings Default instance of settings service that resolves settings from environment variables + * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point + * + * @returns {Promise<*>} A promise that resolves to static settings object + */ +// eslint-disable-next-line no-unused-vars +function getStaticSettings(existingStaticSettings, settings, pluginRegistry) { + const staticSettings = { + ...existingStaticSettings, + }; + + // Register all dynamodb table names used by the base rest api addon + const tablePrefix = settings.get(settingKeys.tablePrefix); + const table = (key, suffix) => { + staticSettings[key] = `${tablePrefix}-${suffix}`; + }; + table('dbTableAuthenticationProviderTypes', 'DbAuthenticationProviderTypes'); + table('dbTableAuthenticationProviderConfigs', 'DbAuthenticationProviderConfigs'); + table('dbTablePasswords', 'DbPasswords'); + table('dbTableUserApiKeys', 'DbUserApiKeys'); + table('dbTableRevokedTokens', 'DbRevokedTokens'); + table('dbTableUsers', 'DbUsers'); + table('dbTableLocks', 'DbLocks'); + + return staticSettings; +} + +const plugin = { + getStaticSettings, + // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient + // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient + // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient + // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient + registerServices, +}; + +module.exports = plugin; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js new file mode 100644 index 0000000000..b0ea14fd50 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const CreateRootUserService = require('../steps/create-root-user-service'); +const AddAuthProviders = require('../steps/add-auth-providers'); +const CreateJwtKeyService = require('../steps/create-jwt-key-service'); + +/** + * Returns a map of post deployment steps + * + * @param existingStepsMap Map of existing post deployment steps + * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point. + * + * @returns {Promise<*>} + */ +// eslint-disable-next-line no-unused-vars +async function getSteps(existingStepsMap, pluginRegistry) { + const stepsMap = new Map([ + ...existingStepsMap, + ['createRootUser', new CreateRootUserService()], + ['createJwtKeyService', new CreateJwtKeyService()], + ['addAuthProviders', new AddAuthProviders()], + ]); + + return stepsMap; +} + +const plugin = { + getSteps, +}; + +module.exports = plugin; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json new file mode 100644 index 0000000000..8ccee66fd6 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["type", "id"] +} diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js new file mode 100644 index 0000000000..42cf137707 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const _ = require('lodash'); + +/** + * Utility function to register post-deployment steps by calling each post-deployment step registration plugin in order. + * + * @param {*} container An instance of ServicesContainer + * @param {getPlugins} pluginRegistry A registry that provides plugins registered by various addons for the specified extension point. + * Each 'postDeploymentStep' plugin in the returned array is an object containing "getSteps" method. + * + * @returns {Promise>} + */ +async function registerSteps(container, pluginRegistry) { + const plugins = await pluginRegistry.getPlugins('postDeploymentStep'); + + // 1. Collect steps from all plugins + // + // Ask each plugin to return their steps. Each plugin is passed a Map containing the post deployment steps collected + // so far from other plugins. The plugins are called in the same order as returned by the registry. + // Each plugin gets a chance to add, remove, update, or delete steps by mutating the provided stepsMap object. + // This stepsMap is a Map that has step service names as keys and an instance of step implementation service containing + // "execute" method as value. + // + const stepsMap = await _.reduce( + plugins, + async (stepsSoFarPromise, plugin) => plugin.getSteps(await stepsSoFarPromise, pluginRegistry), + Promise.resolve(new Map()), + ); + + // 2. Register all steps to the container + stepsMap.forEach((stepService, stepName) => { + container.register(stepName, stepService); + }); + + return stepsMap; +} + +module.exports = { registerSteps }; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js new file mode 100644 index 0000000000..7009deb7fc --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js @@ -0,0 +1,182 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const _ = require('lodash'); +const Service = require('@aws-ee/base-services-container/lib/service'); +const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context'); +const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants') + .authenticationProviders; + +const settingKeys = { + awsRegion: 'awsRegion', + envName: 'envName', + solutionName: 'solutionName', + enableNativeUserPoolUsers: 'enableNativeUserPoolUsers', + fedIdpIds: 'fedIdpIds', + fedIdpNames: 'fedIdpNames', + fedIdpDisplayNames: 'fedIdpDisplayNames', + fedIdpMetadatas: 'fedIdpMetadatas', + defaultAuthNProviderTitle: 'defaultAuthNProviderTitle', + cognitoAuthNProviderTitle: 'cognitoAuthNProviderTitle', +}; + +class AddAuthProviders extends Service { + constructor() { + super(); + this.dependency([ + 'aws', + 'authenticationProviderConfigService', + 'authenticationProviderTypeService', + 'cognitoUserPoolAuthenticationProvisionerService', + 'internalAuthenticationProvisionerService', + ]); + } + + async addDefaultAuthenticationProviderConfig() { + const authenticationProviderTypeService = await this.service('authenticationProviderTypeService'); + const authenticationProviderTypes = await authenticationProviderTypeService.getAuthenticationProviderTypes( + getSystemRequestContext(), + ); + + const internalAuthProviderTypeConfig = _.find(authenticationProviderTypes, { + type: authProviderConstants.internalAuthProviderTypeId, + }); + // Each provider can ask for their specific config at the time of registering the provider + // The config below for "internal" provider can be hard-coded. + const defaultAuthProviderConfig = { + id: authProviderConstants.internalAuthProviderId, + title: this.settings.get(settingKeys.defaultAuthNProviderTitle), + signInUri: 'api/authentication/id-tokens', + // signOutUri: '', + }; + + const internalAuthenticationProvisionerService = await this.service('internalAuthenticationProvisionerService'); + await internalAuthenticationProvisionerService.provision({ + providerTypeConfig: internalAuthProviderTypeConfig, + providerConfig: defaultAuthProviderConfig, + }); + } + + /** + * Configure Cognito Authentication Provider. The step method below invokes the cognito auth provider "Provisioner" service. + * The service will do the followings + * 1. Create cognito user pool, if it doesn't exist + * 2. Create and configure application client for this solution in the cognito user pool + * 3. Configure identity providers in the cognito user pool + * 4. Configure cognito user pool domain for the client application + */ + async addCognitoAuthenticationProviderWithSamlFederation() { + // Get settings + const envName = this.settings.get(settingKeys.envName); + const solutionName = this.settings.get(settingKeys.solutionName); + + const enableNativeUserPoolUsers = this.settings.getBoolean(settingKeys.enableNativeUserPoolUsers); + + const fedIdpIds = this.settings.optionalObject(settingKeys.fedIdpIds, []); + const fedIdpNames = this.settings.optionalObject(settingKeys.fedIdpNames, []); + const fedIdpDisplayNames = this.settings.optionalObject(settingKeys.fedIdpDisplayNames, []); + const fedIdpMetadatas = this.settings.optionalObject(settingKeys.fedIdpMetadatas, []); + + // If user pools aren't enabled and no IdPs are configured, skip user pool creation + const idpsConfigured = [fedIdpIds, fedIdpNames, fedIdpDisplayNames, fedIdpMetadatas].some( + array => array.length === 0, + ); + if (!enableNativeUserPoolUsers && idpsConfigured) { + this.log.info('Cognito user pool not enabled in settings; skipping creation'); + return; + } + + // Construct base auth provider config + const federatedIdentityProviders = await Promise.all( + fedIdpIds.map(async (idpId, idx) => { + return { + id: idpId, + name: fedIdpNames[idx], + displayName: fedIdpDisplayNames[idx], + metadata: fedIdpMetadatas[idx], + }; + }), + ); + + const userPoolName = `${envName}-${solutionName}-userPool`; + const cognitoAuthProviderConfig = { + title: this.settings.get(settingKeys.cognitoAuthNProviderTitle), + userPoolName, + clientName: `${envName}-${solutionName}-client`, + userPoolDomain: `${envName}-${solutionName}`, + enableNativeUserPoolUsers, + federatedIdentityProviders, + }; + + // Define auth provider type config + const authenticationProviderTypeService = await this.service('authenticationProviderTypeService'); + const authenticationProviderTypes = await authenticationProviderTypeService.getAuthenticationProviderTypes( + getSystemRequestContext(), + ); + + const cognitoAuthProviderTypeConfig = _.find(authenticationProviderTypes, { + type: authProviderConstants.cognitoAuthProviderTypeId, + }); + + // Check whether user pool already exists + const aws = await this.service('aws'); + const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider(); + // TODO: Handle pagination (hopefully there aren't more than 1000 user pools) + const result = await cognitoIdentityServiceProvider.listUserPools({ MaxResults: '60' }).promise(); + const userPool = _.find(result.UserPools, { Name: userPoolName }); + + let authProviderExists = false; + if (userPool) { + // If pool exists, set its ID in the config so it can be updated + cognitoAuthProviderConfig.userPoolId = userPool.Id; + + // Verify that the stored auth provider config also exists + const awsRegion = this.settings.get(settingKeys.awsRegion); + const authProviderId = `https://cognito-idp.${awsRegion}.amazonaws.com/${userPool.Id}`; + + const authenticationProviderConfigService = await this.service('authenticationProviderConfigService'); + authProviderExists = !!(await authenticationProviderConfigService.getAuthenticationProviderConfig( + authProviderId, + )); + + if (authProviderExists) { + cognitoAuthProviderConfig.id = authProviderId; + } + } + + // Create or update user pool + const action = authProviderExists + ? authProviderConstants.provisioningAction.update + : authProviderConstants.provisioningAction.create; + + const cognitoAuthenticationProvisionerService = await this.service( + 'cognitoUserPoolAuthenticationProvisionerService', + ); + await cognitoAuthenticationProvisionerService.provision({ + providerTypeConfig: cognitoAuthProviderTypeConfig, + providerConfig: cognitoAuthProviderConfig, + action, + }); + } + + async execute() { + // Setup both the default (internal) auth provider as well as a Cognito + // auth provider (if configured) + await this.addDefaultAuthenticationProviderConfig(); + await this.addCognitoAuthenticationProviderWithSamlFederation(); + } +} + +module.exports = AddAuthProviders; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js new file mode 100644 index 0000000000..4eb8202b9d --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js @@ -0,0 +1,87 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const Service = require('@aws-ee/base-services-container/lib/service'); +const passwordGenerator = require('generate-password'); + +const settingKeys = { + paramStoreJwtSecret: 'paramStoreJwtSecret', + solutionName: 'solutionName', +}; + +class CreateJwtKeyService extends Service { + constructor() { + super(); + this.dependency(['aws']); + } + + generatePassword() { + return passwordGenerator.generate({ + length: 12, // 12 characters in password + numbers: true, // include numbers in password + symbols: true, // include symbols + uppercase: true, // include uppercase + strict: true, // make sure to include at least one character from each pool + }); + } + + async createJwtSigningKey() { + const aws = await this.service('aws'); + const ssm = new aws.sdk.SSM({ apiVersion: '2014-11-06' }); + const solutionName = this.settings.get(settingKeys.solutionName); + const paramStoreJwtSecretName = this.settings.get(settingKeys.paramStoreJwtSecret); + + let doesKeyExist = false; + try { + await ssm.getParameter({ Name: paramStoreJwtSecretName, WithDecryption: true }).promise(); + doesKeyExist = true; + } catch (err) { + if (err.code !== 'ParameterNotFound') { + // Swallow "ParameterNotFound" and let all other errors bubble up + throw err; + } + } + + if (doesKeyExist) { + this.log.info( + `JWT signing key already exists in parameter store at ${paramStoreJwtSecretName}. Did not reset it.`, + ); + // TODO: Support resetting JWT key + } else { + // Auto-generate signing key for the jwt tokens + const jwtSigningKey = this.generatePassword(); + + await ssm + .putParameter({ + Name: paramStoreJwtSecretName, + Type: 'SecureString', + Value: jwtSigningKey, + Description: `JWT signing key for ${solutionName}`, + Overwrite: true, + }) + .promise(); + + this.log.info(`Created JWT signing key and saved it to parameter store at ${paramStoreJwtSecretName}`); + } + } + + async execute() { + // The following will create new JWT signing key every time it is executed + // TODO: Do not re-create JWT keys if they already exists, at the same time support for rotating the key + return this.createJwtSigningKey(); + } +} + +module.exports = CreateJwtKeyService; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js new file mode 100644 index 0000000000..8c31047562 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js @@ -0,0 +1,110 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const passwordGenerator = require('generate-password'); +const Service = require('@aws-ee/base-services-container/lib/service'); +const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants') + .authenticationProviders; +const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context'); + +const settingKeys = { + paramStoreJwtSecret: 'paramStoreJwtSecret', + rootUserName: 'rootUserName', + rootUserEmail: 'rootUserEmail', + rootUserFirstName: 'rootUserFirstName', + rootUserLastName: 'rootUserLastName', + rootUserPasswordParamName: 'rootUserPasswordParamName', + solutionName: 'solutionName', +}; + +class CreateRootUserService extends Service { + constructor() { + super(); + this.dependency(['userService', 'dbPasswordService', 'aws']); + } + + async createRootUser() { + const rootUserName = this.settings.get(settingKeys.rootUserName); + const rootUserEmail = this.settings.get(settingKeys.rootUserEmail); + const rootUserFirstName = this.settings.get(settingKeys.rootUserFirstName); + const rootUserLastName = this.settings.get(settingKeys.rootUserLastName); + const rootUserPasswordParamName = this.settings.get(settingKeys.rootUserPasswordParamName); + const solutionName = this.settings.get(settingKeys.solutionName); + + // Auto-generate password for the root user + const rootUserPassword = this.generatePassword(); + + const [userService, dbPasswordService, aws] = await this.service(['userService', 'dbPasswordService', 'aws']); + + try { + await userService.createUser(getSystemRequestContext(), { + username: rootUserName, + authenticationProviderId: authProviderConstants.internalAuthProviderId, + firstName: rootUserFirstName, + lastName: rootUserLastName, + email: rootUserEmail, + isAdmin: true, + userType: 'root', + }); + this.log.info('Created root user in the data lake'); + + await dbPasswordService.savePassword(getSystemRequestContext(), { + username: rootUserName, + password: rootUserPassword, + }); + this.log.info("Created root user's password"); + + const ssm = new aws.sdk.SSM({ apiVersion: '2014-11-06' }); + await ssm + .putParameter({ + Name: rootUserPasswordParamName, + Type: 'SecureString', + Value: rootUserPassword, + Description: `root user password for the ${solutionName}`, + Overwrite: true, + }) + .promise(); + this.log.info(`Created root user with user name = ${rootUserName}`); + this.log.info(`Please find the root user's password in parameter store at = ${rootUserPasswordParamName}`); + } catch (err) { + if (err.code === 'alreadyExists') { + // TODO: Allow updating root users information in post-deployment + // The root user already exists. Nothing to do. + this.log.info( + `The root user with user name = ${rootUserName} already exists. Did NOT overwrite that user's information.`, + ); + } else { + // In case of any other error let it bubble up + throw err; + } + } + } + + generatePassword() { + return passwordGenerator.generate({ + length: 12, // 12 characters in password + numbers: true, // include numbers in password + symbols: true, // include symbols + uppercase: true, // include uppercase + strict: true, // make sure to include at least one character from each pool + }); + } + + async execute() { + return this.createRootUser(); + } +} + +module.exports = CreateRootUserService; diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/package.json b/addons/addon-base-post-deployment/packages/base-post-deployment/package.json new file mode 100644 index 0000000000..b8c191bb84 --- /dev/null +++ b/addons/addon-base-post-deployment/packages/base-post-deployment/package.json @@ -0,0 +1,45 @@ +{ + "name": "@aws-ee/base-post-deployment", + "version": "1.0.0", + "private": true, + "description": "A library containing base set of post-deployment steps to be run with solutions based on addons", + "author": "Amazon Web Services", + "license": "Apache-2.0", + "dependencies": { + "@aws-ee/base-api-services": "workspace:*", + "@aws-ee/base-services": "workspace:*", + "@aws-ee/base-services-container": "workspace:*", + "aws-sdk": "^2.647.0", + "generate-password": "^1.5.0", + "lodash": "^4.17.15" + }, + "devDependencies": { + "eslint": "^6.8.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.10.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-jest": "^22.21.0", + "eslint-plugin-prettier": "^3.1.2", + "husky": "^3.1.0", + "jest": "^24.9.0", + "jest-junit": "^10.0.0", + "prettier": "^1.19.1", + "source-map-support": "^0.5.16" + }, + "scripts": { + "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests", + "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll", + "lint": "pnpm run lint:eslint && pnpm run lint:prettier", + "lint:eslint": "eslint --ignore-path .gitignore . ", + "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ", + "format": "pnpm run format:eslint && pnpm run format:prettier", + "format:eslint": "eslint --fix --ignore-path .gitignore . ", + "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' " + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'" + } + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc b/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc new file mode 100644 index 0000000000..83064a1e60 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc @@ -0,0 +1,4 @@ +{ + "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]], + "presets": ["@babel/preset-react"] +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json b/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json new file mode 100644 index 0000000000..90ac399dcf --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json @@ -0,0 +1,42 @@ +{ + "extends": ["airbnb", "prettier", "prettier/react", "plugin:jest/recommended", "eslint-config-prettier"], + "parser": "babel-eslint", + "plugins": ["jest", "prettier", "react"], + "rules": { + "prettier/prettier": ["error"], + "no-unused-vars": [ + "error", + { + "varsIgnorePattern": "^_.+", + "argsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "args": "after-used", + "ignoreRestSiblings": true + } + ], + "prefer-destructuring": 0, + "no-underscore-dangle": 0, + "no-param-reassign": 0, + "class-methods-use-this": 0, + "no-use-before-define": 0, + "no-console": 0, + "no-plusplus": 0, + "no-nested-ternary": 0, + "import/no-named-as-default": 0, + "import/no-named-as-default-member": 0, + "react/jsx-props-no-spreading": 0, + "react/jsx-one-expression-per-line": 0, + "react/jsx-filename-extension": 0, + "react/destructuring-assignment": 0, + "react/sort-comp": 0, + "react/prop-types": 0, + "jsx-a11y/no-static-element-interactions": 0, + "jsx-a11y/click-events-have-key-events": 0 + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "jest/globals": true + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore b/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore new file mode 100644 index 0000000000..49cc63674e --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +**/.class +**/.DS_Store +**/node_modules + +**/npm-debug.log +**/pnpm-debug.log +**/npm-debug.log* + +# Serverless directories +.serverless + +# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dependencies +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# transpiled code +dist + +# misc +.env.local +.env.development +.env.development.local +.env.test.local +.env.production +.env.production.local + +yarn-debug.log* +yarn-error.log* + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json b/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json new file mode 100644 index 0000000000..a333103711 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "printWidth": 120, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "all" +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg new file mode 100644 index 0000000000..8fde615e23 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg @@ -0,0 +1 @@ +Amazon-Simple-Storage-Service-S3_Bucket-with-Objects_dark-bg \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png new file mode 100644 index 0000000000..721b1bb2e2 Binary files /dev/null and b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png differ diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg new file mode 100644 index 0000000000..526fe3d883 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg @@ -0,0 +1 @@ +ec2-icon \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg new file mode 100644 index 0000000000..d6901800e2 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg @@ -0,0 +1 @@ +ecs-icon \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg new file mode 100644 index 0000000000..f870e48465 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg @@ -0,0 +1 @@ +emr-icon \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg new file mode 100644 index 0000000000..a9e54a5032 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg @@ -0,0 +1 @@ +Amazon-SageMaker_Notebook_light-bg \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js b/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js new file mode 100644 index 0000000000..60c221fc1d --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// jest.config.js +module.exports = { + // verbose: true, + notify: false, + testEnvironment: 'node', + // testPathIgnorePatterns: ['service.test.js'], + // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports + // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html + reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]], +}; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json b/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json new file mode 100644 index 0000000000..780d3afae6 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/package.json b/addons/addon-base-raas-ui/packages/base-raas-ui/package.json new file mode 100644 index 0000000000..332cd3593d --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/package.json @@ -0,0 +1,98 @@ +{ + "name": "@aws-ee/base-raas-ui", + "version": "0.1.0", + "private": true, + "author": "Amazon Web Services", + "license": "Apache-2.0", + "dependencies": { + "@auth0/auth0-spa-js": "^1.2.3", + "@aws-ee/base-ui": "workspace:*", + "aws-sdk": "^2.647.0", + "chart.js": "^2.9.3", + "classnames": "^2.2.6", + "crypto-browserify": "^3.12.0", + "csvtojson": "^2.0.10", + "is-cidr": "^3.1.0", + "jwt-decode": "^2.2.0", + "lodash": "^4.17.15", + "mobx": "^5.15.4", + "mobx-react": "^6.1.7", + "mobx-react-form": "^2.0.8", + "mobx-state-tree": "^3.15.0", + "numeral": "^2.0.6", + "pretty-bytes": "^5.3.0", + "prop-types": "^15.7.2", + "react": "^16.12.0", + "react-avatar": "^3.9.0", + "react-chartjs-2": "^2.9.0", + "react-copy-to-clipboard": "^5.0.2", + "react-dom": "^16.12.0", + "react-dotdotdot": "^1.3.1", + "react-dropzone": "^10.1.9", + "react-router-dom": "^5.1.2", + "react-select": "^3.0.8", + "react-sparklines": "^1.7.0", + "react-syntax-highlighter": "^11.0.2", + "react-table": "^6.11.5", + "react-timeago": "^4.4.0", + "semantic-ui-react": "^0.88.2", + "showdown": "^1.9.1", + "toastr": "^2.1.4", + "typeface-lato": "0.0.75", + "uuid": "^3.4.0", + "validatorjs": "^3.18.1", + "request": "^2.34" + }, + "devDependencies": { + "@babel/cli": "^7.8.4", + "@babel/core": "^7.8.6", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-transform-react-jsx": "^7.8.3", + "@babel/preset-env": "^7.8.6", + "@babel/preset-react": "^7.8.3", + "babel-eslint": "^10.0.3", + "eslint": "^6.8.0", + "eslint-config-airbnb": "^18.0.1", + "eslint-config-prettier": "^6.10.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-jest": "^22.21.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-react": "^7.18.3", + "eslint-plugin-react-hooks": "^1.7.0", + "husky": "^3.1.0", + "jest": "^24.9.0", + "jest-junit": "^10.0.0", + "prettier": "^1.19.1", + "pretty-quick": "^1.11.1", + "prop-types": "^15.7.2", + "serverless": "^1.63.0", + "serverless-deployment-bucket": "^1.1.0", + "typescript": "^3.7.5", + "webpack": "4.41.2" + }, + "scripts": { + "babel": "babel src/ --out-dir dist/ --source-maps", + "babel:watch": "babel src/ --out-dir dist/ --source-maps --watch", + "build": "pnpm run babel", + "build:watch": "pnpm run babel:watch", + "lint": "pnpm run lint:eslint && pnpm run lint:prettier", + "lint:eslint": "eslint --ignore-path .gitignore . ", + "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ", + "format": "pnpm run format:eslint && pnpm run format:prettier", + "format:eslint": "eslint --fix --ignore-path .gitignore . ", + "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' ", + "prepare": "pnpm run build" + }, + "files": [ + "README.md", + "dist/", + "src/" + ], + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'" + } + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js new file mode 100644 index 0000000000..2f36cd4c62 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js @@ -0,0 +1,247 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import { httpApiGet, httpApiPost, httpApiPut, httpApiDelete } from '@aws-ee/base-ui/dist/helpers/api'; + +function getUserRoles() { + return httpApiGet('api/user-roles'); +} + +function getAwsAccounts() { + return httpApiGet('api/aws-accounts'); +} + +function getAwsAccount(accountId) { + return httpApiGet(`api/aws-accounts/${accountId}`); +} + +function addUsers(users) { + return httpApiPost('api/users/bulk', { data: users }); +} + +function addAwsAccount(awsAccount) { + return httpApiPost('api/aws-accounts', { data: awsAccount }); +} + +function createAwsAccount(awsAccount) { + return httpApiPost('api/aws-accounts/provision', { data: awsAccount }); +} + +function addIndex(index) { + return httpApiPost('api/indexes', { data: index }); +} + +function updateUserApplication(user) { + // const params = {}; + // if (user.authenticationProviderId) { + // params.authenticationProviderId = user.authenticationProviderId; + // } + // if (user.identityProviderName) { + // params.identityProviderName = user.identityProviderName; + // } + // return httpApiPut(`api/users/${user.username}/userself`, { data: user, params }); + return httpApiPut(`api/user`, { data: user }); +} + +async function deleteUser(user) { + const data = {}; + if (user.authenticationProviderId) { + data.authenticationProviderId = user.authenticationProviderId; + } + if (user.identityProviderName) { + data.identityProviderName = user.identityProviderName; + } + return httpApiDelete(`api/users/${user.username}`, { data }); +} + +function getStudies(category) { + return httpApiGet('api/studies', { params: { category } }); +} + +function getStudy(id) { + return httpApiGet(`api/studies/${id}`); +} + +function createStudy(body) { + return httpApiPost('api/studies', { data: body }); +} + +function listStudyFiles(studyId) { + return httpApiGet(`api/studies/${studyId}/files`); +} + +function getPresignedStudyUploadRequests(studyId, filenames) { + if (Array.isArray(filenames)) { + filenames = filenames.join(','); + } + return httpApiGet(`api/studies/${studyId}/upload-requests`, { params: { filenames } }); +} + +function getStudyPermissions(studyId) { + return httpApiGet(`api/studies/${studyId}/permissions`); +} + +function updateStudyPermissions(studyId, updateRequest) { + return httpApiPut(`api/studies/${studyId}/permissions`, { data: updateRequest }); +} + +async function getStepTemplates() { + return httpApiGet('api/step-templates'); +} + +function getEnvironments() { + return httpApiGet('api/workspaces'); +} + +function getEnvironmentCost(id, numberDaysInPast, groupByService = true, groupByUser = false) { + return httpApiGet( + `api/costs?env=${id}&numberOfDaysInPast=${numberDaysInPast}&groupByService=${groupByService}&groupByUser=${groupByUser}`, + ); +} + +function getAllProjCostGroupByUser(numberDaysInPast) { + return httpApiGet(`api/costs?proj=ALL&groupByUser=true&numberOfDaysInPast=${numberDaysInPast}`); +} + +function getAllProjCostGroupByEnv(numberDaysInPast) { + return httpApiGet(`api/costs?proj=ALL&groupByEnv=true&numberOfDaysInPast=${numberDaysInPast}`); +} + +function getEnvironment(id) { + return httpApiGet(`api/workspaces/${id}`); +} + +function deleteEnvironment(id) { + return httpApiDelete(`api/workspaces/${id}`); +} + +function createEnvironment(body) { + return httpApiPost('api/workspaces', { data: body }); +} + +function updateEnvironment(body) { + return httpApiPut('api/workspaces', { data: body }); +} + +function getEnvironmentKeypair(id) { + return httpApiGet(`api/workspaces/${id}/keypair`); +} + +function getEnvironmentPasswordData(id) { + return httpApiGet(`api/workspaces/${id}/password`); +} + +function getEnvironmentNotebookUrl(id) { + return httpApiGet(`api/workspaces/${id}/url`); +} + +function getEnvironmentSpotPriceHistory(type) { + return httpApiGet(`api/workspaces/pricing/${type}`); +} + +function getExternalTemplate(key) { + return httpApiGet(`api/template/${key}`); +} + +function getIndexes() { + return httpApiGet('api/indexes'); +} + +function getIndex(indexId) { + return httpApiGet(`api/indexes/${indexId}`); +} + +function getProjects() { + return httpApiGet('api/projects'); +} + +function getProject(id) { + return httpApiGet(`api/projects/${id}`); +} + +function addProject(project) { + return httpApiPost('api/projects', { data: project }); +} + +function getAccounts() { + return httpApiGet('api/accounts'); +} + +function getAccount(id) { + return httpApiGet(`api/accounts/${id}`); +} + +function removeAccountInfo(id) { + return httpApiDelete(`api/accounts/${id}`); +} + +function getComputePlatforms() { + return httpApiGet(`api/compute/platforms`); +} + +function getComputeConfigurations(platformId) { + return httpApiGet(`api/compute/platforms/${platformId}/configurations`); +} + +function getClientIpAddress() { + return httpApiGet(`api/ip`); +} + +// API Functions Insertion Point (do not change this text, it is being used by hygen cli) + +export { + addIndex, + addUsers, + removeAccountInfo, + deleteUser, + getUserRoles, + getAwsAccounts, + getAwsAccount, + getStudies, + getStudy, + createStudy, + listStudyFiles, + getPresignedStudyUploadRequests, + getStudyPermissions, + updateStudyPermissions, + addAwsAccount, + createAwsAccount, + getStepTemplates, + getEnvironments, + getEnvironment, + getEnvironmentCost, + deleteEnvironment, + createEnvironment, + updateEnvironment, + getEnvironmentKeypair, + getEnvironmentPasswordData, + getEnvironmentNotebookUrl, + getEnvironmentSpotPriceHistory, + getExternalTemplate, + getAllProjCostGroupByUser, + getIndexes, + getIndex, + getAllProjCostGroupByEnv, + updateUserApplication, + getProjects, + getProject, + addProject, + getAccounts, + getAccount, + getComputePlatforms, + getComputeConfigurations, + getClientIpAddress, +}; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js new file mode 100644 index 0000000000..15a5d2fa65 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js @@ -0,0 +1,113 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const _ = require('lodash'); +const aws = require('aws-sdk'); + +const STACK_FAIL = [ + 'CREATE_FAILED', + 'ROLLBACK_FAILED', + 'DELETE_FAILED', + 'UPDATE_ROLLBACK_FAILED', + 'ROLLBACK_COMPLETE', + 'UPDATE_ROLLBACK_COMPLETE', +]; +const STACK_SUCCESS = ['CREATE_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE']; + +export default class CfnService { + constructor(accessKeyId, secretAccessKey, region = 'us-east-1') { + if (accessKeyId) { + this.cfn = new aws.CloudFormation({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }); + } else { + this.cfn = new aws.CloudFormation({ + sslEnabled: true, + }); + } + } + + isDone(status) { + return STACK_FAIL.includes(status) || STACK_SUCCESS.includes(status); + } + + static async validateCredentials(accessKeyId, secretAccessKey) { + const sts = new aws.STS({ + accessKeyId, + secretAccessKey, + sslEnabled: true, + }); + + return sts.getCallerIdentity().promise(); + } + + async describeStack(stackName) { + const params = { StackName: stackName }; + + try { + const response = await this.cfn.describeStacks(params).promise(); + const stack = _.get(response, 'Stacks[0]'); + const status = _.get(stack, 'StackStatus', 'Unknown'); + const statusReason = _.get(stack, 'StackStatusReason', 'Unknown'); + const outputs = _.get(stack, 'Outputs', []); + const outputsNormalized = _.map(outputs, item => ({ + key: item.OutputKey, + value: item.OutputValue, + description: item.Description, + exportName: item.ExportName, + })); + + return { + status, + statusReason, + isDone: this.isDone(status), + isFailed: STACK_FAIL.includes(status), + outputs: outputsNormalized, + }; + } catch (e) { + throw new Error(`${e.code}: ${e.message}`); + } + } + + async createStack(stackName, cfnParams, templateUrl, description = '') { + const input = { + StackName: stackName, + Parameters: cfnParams, + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], + TemplateURL: templateUrl, + Tags: [ + { + Key: 'Description', + Value: description, + }, + ], + }; + + return this.cfn.createStack(input).promise(); + } + + async deleteStack(stackName) { + const input = { + StackName: stackName, + }; + + const response = await this.cfn.deleteStack(input).promise(); + + return response; + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js new file mode 100644 index 0000000000..65e04f18e5 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt(). + * https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a + * MIT lisence + * + * @param {String} plaintext - Plaintext to be encrypted. + * @param {String} password - Password to use to encrypt plaintext. + * @returns {String} Encrypted ciphertext. + * + * @example + * const ciphertext = await aesGcmEncrypt('my secret text', 'pw'); + * aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); }); + */ +async function aesGcmEncrypt(plaintext, password) { + const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8 + const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password + + const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv + + const alg = { name: 'AES-GCM', iv }; // specify algorithm to use + + const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw + + const ptUtf8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8 + const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUtf8); // encrypt plaintext using key + + const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array + const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string + const ctBase64 = btoa(ctStr); // encode ciphertext as base64 + + const ivHex = Array.from(iv) + .map(b => `00${b.toString(16)}`.slice(-2)) + .join(''); // iv as hex string + + return ivHex + ctBase64; // return iv+ciphertext +} + +/** + * Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password. + * https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a + * MIT lisence + * + * @param {String} ciphertext - Ciphertext to be decrypted. + * @param {String} password - Password to use to decrypt ciphertext. + * @returns {String} Decrypted plaintext. + * + * @example + * const plaintext = await aesGcmDecrypt(ciphertext, 'pw'); + * aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); }); + */ +async function aesGcmDecrypt(ciphertext, password) { + const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8 + const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password + + const iv = ciphertext + .slice(0, 24) + .match(/.{2}/g) + .map(byte => parseInt(byte, 16)); // get iv from ciphertext + + const alg = { name: 'AES-GCM', iv: new Uint8Array(iv) }; // specify algorithm to use + + const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // use pw to generate key + + const ctStr = atob(ciphertext.slice(24)); // decode base64 ciphertext + const ctUint8 = new Uint8Array(ctStr.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array + + const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key + const plaintext = new TextDecoder().decode(plainBuffer); // decode password from UTF-8 + + return plaintext; // return the plaintext +} + +export { aesGcmEncrypt, aesGcmDecrypt }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js new file mode 100644 index 0000000000..37c17c8abc --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; + +const codes = ['apiError', 'notFound', 'badRequest', 'tokenExpired', 'incorrectImplementation', 'timeout']; + +const boom = { + error: (friendlyOrErr, code, friendly = '') => { + if (_.isString(friendlyOrErr)) { + const e = new Error(friendlyOrErr); + e.isBoom = true; + e.code = code; + e.friendly = friendlyOrErr; // the friendly argument is ignored and friendlyOrErr is used instead + return e; + } + if (_.isError(friendlyOrErr)) { + friendlyOrErr.code = code; // eslint-disable-line no-param-reassign + friendlyOrErr.isBoom = true; // eslint-disable-line no-param-reassign + friendlyOrErr.friendly = friendly || _.startCase(code); + return friendlyOrErr; + } + + // if we are here, it means that the msgOrErr is an object + const err = new Error(JSON.stringify(friendlyOrErr)); + err.isBoom = true; + err.code = code; + err.friendly = friendly || _.startCase(code); + + return err; + }, +}; + +// inject all the codes array elements as properties for the boom +// example 'apiError' injected => produces boom.apiError(errOrFriendlyMsg, friendlyMsg) +// then you can call boom.apiError(err, 'Error fetching user info') +codes.forEach(code => { + boom[code] = (errOrFriendlyMsg, friendlyMsg) => boom.error(errOrFriendlyMsg, code, friendlyMsg); +}); + +const isNotFound = error => { + return _.get(error, 'code') === 'notFound'; +}; + +const isTokenExpired = error => { + return _.get(error, 'code') === 'tokenExpired'; +}; + +const isForbidden = error => { + return _.get(error, 'code') === 'forbidden'; +}; + +export { boom, isNotFound, isTokenExpired, isForbidden }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js new file mode 100644 index 0000000000..2222c8d138 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const STS = require('aws-sdk/clients/sts'); + +export default function getAccountDetails({ accessKeyId, secretAccessKey, region = 'us-east-1' }) { + const sts = accessKeyId + ? new STS({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }) + : new STS({ sslEnabled: true }); + + return sts.getCallerIdentity().promise(); +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js new file mode 100644 index 0000000000..fa33d61a1c --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { EnvironmentConfigurationsStore } from '../models/environments/EnvironmentConfigurationsStore'; + +const getEstimatedCost = async (env, numberOfDaysToGetCostInfo) => { + const envConfig = EnvironmentConfigurationsStore.create(); + await envConfig.load(); + const allEnvConfigs = envConfig.list; + const config = _.find(allEnvConfigs, conf => { + // Hail EMR has spot pricing and on demand price. o we need to pick the correct EMR env config + if (env.instanceInfo.type === 'emr') { + if (env.instanceInfo.config.spotBidPrice) { + return ( + conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size && conf.label.includes('Spot') + ); + } + return ( + conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size && conf.label.includes('On Demand') + ); + } + return conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size; + }); + const cost = {}; + cost[config.type] = { + amount: config.totalPrice, + unit: 'USD', + }; + + const allCost = []; + for (let i = numberOfDaysToGetCostInfo; i > 0; i--) { + const day = new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + const costForDay = { + startDate: day, + cost, + }; + allCost.push(costForDay); + } + return allCost; +}; + +// eslint-disable-next-line import/prefer-default-export +export { getEstimatedCost }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js new file mode 100644 index 0000000000..8d61620fb4 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const EC2 = require('aws-sdk/clients/ec2'); +const SSM = require('aws-sdk/clients/ssm'); + +const paramStoreRoot = 'raas'; + +export default class EnvironmentKeypairService { + constructor({ accessKeyId, secretAccessKey, region = 'us-east-1' }) { + if (accessKeyId) { + this.ec2 = new EC2({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }); + this.ssm = new SSM({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }); + } else { + this.ec2 = new EC2({ + sslEnabled: true, + }); + this.ssm = new SSM({ + sslEnabled: true, + }); + } + } + + async create(id) { + const keyPair = await this.ec2.createKeyPair({ KeyName: id }).promise(); + + const parameterName = `/${paramStoreRoot}/environments/${id}`; + await this.ssm + .putParameter({ + Name: parameterName, + Type: 'SecureString', + Value: keyPair.KeyMaterial, + Description: `ssh key for environment ${id}`, + Overwrite: true, + }) + .promise(); + + return keyPair.KeyName; + } + + async mustFind(id) { + const parameterName = `/${paramStoreRoot}/environments/${id}`; + const privateKey = await this.ssm + .getParameter({ + Name: parameterName, + WithDecryption: true, + }) + .promise(); + + return { privateKey: privateKey.Parameter.Value }; + } + + async delete(id) { + const parameterName = `/${paramStoreRoot}/environments/${id}`; + + await this.ec2.deleteKeyPair({ KeyName: id }).promise(); + + await this.ssm + .deleteParameter({ + Name: parameterName, + }) + .promise(); + + return true; + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js new file mode 100644 index 0000000000..bfc40188e7 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const EC2 = require('aws-sdk/clients/ec2'); + +export default class SageMakerService { + constructor({ accessKeyId, secretAccessKey, region = 'us-east-1' }) { + if (accessKeyId) { + this.ec2 = new EC2({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }); + } else { + this.ec2 = new EC2({ + sslEnabled: true, + }); + } + } + + async defaultVPCInfo() { + const { Vpcs: vpcs } = await this.ec2 + .describeVpcs({ + Filters: [ + { + Name: 'isDefault', + Values: ['true'], + }, + ], + }) + .promise(); + const { VpcId: vpcId } = vpcs.find(({ IsDefault }) => IsDefault); + + const { Subnets: subnets } = await this.ec2 + .describeSubnets({ + Filters: [ + { + Name: 'vpc-id', + Values: [vpcId], + }, + ], + }) + .promise(); + + // Default subnets should be public, but just make sure + const publicSubnets = subnets.filter(({ MapPublicIpOnLaunch }) => MapPublicIpOnLaunch); + const { SubnetId: subnetId } = publicSubnets.reduce((result, subnet) => + subnet.AvailableIpAddressCount > result.AvailableIpAddressCount ? subnet : result, + ); + return { vpcId, subnetId }; + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js new file mode 100644 index 0000000000..125ae28dba --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import dvr from 'mobx-react-form/lib/validators/DVR'; +import validatorjs from 'validatorjs'; +import MobxReactForm from 'mobx-react-form'; +import isCidr from 'is-cidr'; +import * as baseFormHelper from '@aws-ee/base-ui/dist/helpers/form'; + +const dvrRules = { + cidr: { + validatorFn: value => { + const result = isCidr(value); + return result === 4 || result === 6; + }, + message: 'The :attribute is not in the CIDR format.', + }, +}; + +// Extend base formPlugins and add support for "cidr" validation rule +const formPlugins = { + ...baseFormHelper.formPlugins, + dvr: dvr({ + package: validatorjs, + extend: ({ validator }) => { + Object.keys(dvrRules).forEach(key => validator.register(key, dvrRules[key].validatorFn, dvrRules[key].message)); + }, + }), +}; + +const formOptions = baseFormHelper.formOptions; + +function createForm(fields, pluginsParam, optionsParam) { + const plugins = pluginsParam || formPlugins; + const options = optionsParam || formOptions; + return new MobxReactForm({ fields }, { plugins, options }); +} + +const createSingleFieldForm = baseFormHelper.createSingleFieldForm; + +export { formPlugins, formOptions, createForm, createSingleFieldForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js new file mode 100644 index 0000000000..d427d3b739 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const SageMaker = require('aws-sdk/clients/sagemaker'); + +export default class SageMakerService { + constructor(accessKeyId, secretAccessKey, region = 'us-east-1') { + if (accessKeyId) { + this.sm = new SageMaker({ + accessKeyId, + secretAccessKey, + region, + sslEnabled: true, + }); + } else { + this.sm = new SageMaker({ + sslEnabled: true, + }); + } + } + + async getPresignedNotebookInstanceUrl(notebookInstanceName) { + const params = { + NotebookInstanceName: notebookInstanceName, + }; + return this.sm.createPresignedNotebookInstanceUrl(params).promise(); + } +} diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js new file mode 100644 index 0000000000..4ddd228836 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * @typedef {Object} UploadHandle + * @property {Promise} done a Promise that resolves if the upload completes, or rejects if there is an upload error. + * @property {() => void} cancel cancels the upload by calling XMLHttpRequest.abort(). + * @property {(callback: (uploadedBytes: number) => void) => void} onProgress used to register event listeners for upload progress events + */ + +/** + * Uploads an HTML file object or a blob using XMLHttpRequest. + * + * @params {File|Blob} file + * @params {sring} url + * @params {Object} fields + * @returns {UploadHandle} + */ +const upload = (file, url, fields = {}) => { + const req = new XMLHttpRequest(); + const uploadProgressListeners = []; + const uploadProgressCallback = uploadedBytes => { + uploadProgressListeners.forEach(fn => { + fn(uploadedBytes); + }); + }; + + const done = new Promise((resolve, reject) => { + req.upload.addEventListener('progress', event => { + uploadProgressCallback(event.loaded || 0); + }); + req.upload.addEventListener('error', () => { + reject(new Error('Network Error')); + }); + req.onreadystatechange = () => { + if (req.readyState === 4) { + // Request is DONE + if (req.status === 0) { + // Request status is UNSENT + reject(new Error('Cancelled')); + } else if (req.status >= 400 && req.status <= 599) { + // Request received 4xx or 5xx error + reject(new Error(`Error: ${req.statusText}`)); + } else { + resolve(); + } + } + }; + }); + + const formData = new FormData(); + Object.entries(fields).forEach(([name, value]) => formData.append(name, value)); + formData.append('file', file, file.name); + + req.open('POST', url); + req.send(formData); + + return { + done, + cancel() { + req.abort(); + }, + onProgress(cb) { + uploadProgressListeners.push(cb); + }, + }; +}; + +export default upload; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js new file mode 100644 index 0000000000..c50ed70ba7 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import _ from 'lodash'; +import { getEnv, getType } from 'mobx-state-tree'; + +function createAppType(appContext) { + const ParentApp = getType(appContext.app); + + const AppType = ParentApp.named('RaasApp') + .props({ + userRegistered: false, + }) + .actions(self => { + // save the base implementations of the parent app + const superInit = self.init; + const superCleanup = self.cleanup; + + return { + init: async payload => { + await superInit(payload); + self.runInAction(() => { + const userStore = getEnv(self).userStore; + if (_.get(userStore, 'user.status') === 'active') { + self.setUserRegistered(true); + } + }); + }, + + setUserRegistered(flag) { + self.userRegistered = flag; + }, + + // this method is called by the Cleaner + cleanup() { + self.setUserRegistered(false); + superCleanup(); + }, + }; + }); + + return AppType; +} + +function registerContextItems(appContext) { + const App = createAppType(appContext); + appContext.app = App.create({}, appContext); +} + +export { registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js new file mode 100644 index 0000000000..aac0e82c25 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types, applySnapshot } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +const CfnInfo = types.model({ + crossAccountExecutionRoleArn: '', + subnetId: '', + vpcId: '', + stackId: '', +}); +// ================================================================== +// Account +// ================================================================== +const Account = types + .model('Account', { + id: types.identifier, + accountName: '', + status: '', + accountArn: '', + email: '', + cfnInfo: types.optional(CfnInfo, {}), + rev: types.maybe(types.number), + name: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .actions(self => ({ + setAccount(rawAccount) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, rawAccount); + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + })); + +// eslint-disable-next-line import/prefer-default-export +export { Account }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.js new file mode 100644 index 0000000000..2a34982d2b --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.js @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { getParent } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getAccount } from '../../helpers/api'; + +// ================================================================== +// AccountStore +// ================================================================== +const AccountStore = BaseStore.named('AccountStore') + .props({ + accountId: '', + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const parent = getParent(self, 2); + const rawAccount = await getAccount(self.accountId); + parent.addAccount(rawAccount); + return undefined; + }, + + cleanup: () => { + superCleanup(); + }, + }; + }) + + .views(self => ({ + get account() { + const parent = getParent(self, 2); + const w = parent.getAccount(self.accountId); + return w; + }, + })); + +// Note: Do NOT register this in the global context, if you want to gain access to an instance +// use accountsStore.getAccountStore() +// eslint-disable-next-line import/prefer-default-export +export { AccountStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js new file mode 100644 index 0000000000..111edd5140 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js @@ -0,0 +1,136 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { getAccounts, removeAccountInfo } from '../../helpers/api'; +import { Account } from './Account'; +import { AccountStore } from './AccountStore'; + +// ================================================================== +// AccountsStore +// ================================================================== +const AccountsStore = BaseStore.named('AccountsStore') + .props({ + accounts: types.optional(types.map(Account), {}), + accountStores: types.optional(types.map(AccountStore), {}), + tickPeriod: 5 * 1000, // 10 sec + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const accounts = await getAccounts(); + // We try to preserve existing accounts data and merge the new data instead + // We could have used self.accounts.replace(), but it will do clear() then merge() + self.runInAction(() => { + consolidateToMap(self.accounts, accounts, (exiting, newItem) => { + exiting.setAccount(newItem); + }); + }); + return undefined; + }, + + addAccount(rawAccount) { + const id = rawAccount.id; + const previous = self.accounts.get(id); + + if (!previous) { + self.accounts.put(rawAccount); + } else { + previous.setAccount(rawAccount); + } + }, + + async removeItem(id) { + const account = self.accounts.get(id); + self.accounts.delete(account); + await removeAccountInfo(id); + }, + + getAccountStore: accountId => { + let entry = self.accountStores.get(accountId); + if (!entry) { + // Lazily create the store + self.accountStores.set(accountId, AccountStore.create({ accountId })); + entry = self.accountStores.get(accountId); + } + + return entry; + }, + + cleanup: () => { + self.accounts.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get listCreatingAccount() { + const result = []; + self.accounts.forEach(account => { + if (account.status === 'PENDING') { + result.push(account); + } + }); + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + get listErrorAccount() { + const result = []; + self.accounts.forEach(account => { + if (account.status === 'FAILED') { + result.push(account); + } + }); + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + get empty() { + return self.accounts.size === 0; + }, + + get total() { + return self.accounts.size; + }, + + get list() { + const result = []; + self.accounts.forEach(account => result.push(account)); + + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + hasAccount(id) { + return self.accounts.has(id); + }, + + getAccount(id) { + return self.accounts.get(id); + }, + })); + +function registerContextItems(appContext) { + appContext.accountsStore = AccountsStore.create({}, appContext); +} + +export { AccountsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.js new file mode 100644 index 0000000000..1e9cd88b35 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.js @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +// ================================================================== +// AwsAccounts +// ================================================================== +const AwsAccount = types + .model('AwsAccounts', { + id: types.identifier, + rev: types.maybe(types.number), + name: '', + description: '', + accountId: '', + externalId: '', + roleArn: '', + vpcId: '', + subnetId: '', + encryptionKeyArn: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .actions(self => ({ + setAwsAccounts(rawAwsAccounts) { + self.id = rawAwsAccounts.id; + self.rev = rawAwsAccounts.rev || self.rev || 0; + self.name = rawAwsAccounts.name || self.name || ''; + self.description = rawAwsAccounts.description || self.description; + self.accountId = rawAwsAccounts.accountId || rawAwsAccounts.accountId; + self.externalId = rawAwsAccounts.externalId || self.externalId; + self.roleArn = rawAwsAccounts.roleArn || self.roleArn; + self.vpcId = rawAwsAccounts.vpcId || self.vpcId; + self.subnetId = rawAwsAccounts.subnetId || self.subnetId; + self.encryptionKeyArn = rawAwsAccounts.encryptionKeyArn || self.encryptionKeyArn; + self.createdAt = rawAwsAccounts.createdAt || self.createdAt; + self.updatedAt = rawAwsAccounts.updatedAt || self.updatedAt; + self.createdBy = rawAwsAccounts.createdBy || self.createdBy; + self.updatedBy = rawAwsAccounts.updatedBy || self.updatedBy; + // we don't update the other fields because they are being populated by a separate store + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + })); + +// eslint-disable-next-line import/prefer-default-export +export { AwsAccount }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js new file mode 100644 index 0000000000..63758a0514 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getAwsAccounts, addAwsAccount, createAwsAccount } from '../../helpers/api'; +import { AwsAccount } from './AwsAccount'; + +// ================================================================== +// AwsAccountsStore +// ================================================================== +const AwsAccountsStore = BaseStore.named('AwsAccountsStore') + .props({ + awsAccounts: types.optional(types.map(AwsAccount), {}), + tickPeriod: 10 * 1000, // 10 sec + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const awsAccounts = (await getAwsAccounts()) || []; + // We try to preserve existing accounts data and merge the new data instead + // We could have used self.accounts.replace(), but it will do clear() then merge() + self.runInAction(() => { + awsAccounts.forEach(awsAccount => { + const awsAccountsModel = AwsAccount.create(awsAccount); + const previous = self.awsAccounts.get(awsAccountsModel.id); + if (!previous) { + self.awsAccounts.set(awsAccountsModel.id, awsAccountsModel); + } else { + previous.setAwsAccounts(awsAccount); + } + }); + }); + return undefined; + }, + + cleanup: () => { + superCleanup(); + }, + + addAwsAccount: async awsAccount => { + const addedAwsAccount = await addAwsAccount(awsAccount); + self.runInAction(() => { + const addedAwsAccountModel = AwsAccount.create(addedAwsAccount); + self.awsAccounts.set(addedAwsAccountModel.id, addedAwsAccountModel); + }); + }, + + createAwsAccount: async awsAccount => { + await createAwsAccount(awsAccount); + }, + }; + }) + + .views(self => ({ + get list() { + const result = []; + // converting map self.users to result array + self.awsAccounts.forEach(awsAccount => { + const res = {}; + res.name = awsAccount.name; + res.accountId = awsAccount.accountId; + res.roleArn = awsAccount.roleArn; + res.description = awsAccount.description; + res.externalId = awsAccount.externalId; + res.vpcId = awsAccount.vpcId; + res.subnetId = awsAccount.subnetId; + res.encryptionKeyArn = awsAccount.encryptionKeyArn; + result.push(res); + }); + return result; + }, + + get dropdownOptions() { + const result = []; + // converting map self.users to result array + self.awsAccounts.forEach(awsAccount => { + const account = {}; + account.key = awsAccount.id; + account.value = awsAccount.id; + // For migration purposes fallback to id if there's no name + account.text = `${awsAccount.description} (${awsAccount.name || awsAccount.id})`; + result.push(account); + }); + return result; + }, + + getNameForAccountId(id) { + const account = self.awsAccounts.get(id); + + // For migration purposes fallback to id if there's no name + if (!account || !account.name) { + return id; + } + + return `${account.name} (${account.accountId})`; + }, + })); + +function registerContextItems(appContext) { + appContext.awsAccountsStore = AwsAccountsStore.create({}, appContext); +} + +export { AwsAccountsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js new file mode 100644 index 0000000000..8eb5385808 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; + +const ClientInformation = types.model('ClientInformation', { + ipAddress: '', +}); + +// eslint-disable-next-line import/prefer-default-export +export { ClientInformation }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js new file mode 100644 index 0000000000..ceae3e1a9e --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getClientIpAddress } from '../../helpers/api'; +import { ClientInformation } from './ClientInformation'; + +// There are situations in which it is useful for the UI to be able to +// determine the IP address that it has, for example so that it can use that +// IP address in a Security Group rule that later restricts access for a given +// compute environment to the user that launched it. So, this store implements +// a "what is my IP address?" feature. +const ClientInformationStore = BaseStore.named('ClientInformationStore') + .props({ + clientInformation: types.optional(ClientInformation, {}), + }) + .actions(self => { + return { + async doLoad() { + const info = await getClientIpAddress(); + const ipAddress = info.ipAddress; + if (ipAddress === '127.0.0.1') { + // Only for "local" development that we call http://httpbin.org/get + // otherwise for any other development modes including for production, + // we call our own api to get the ip address. + const answer = await fetch('http://httpbin.org/get').then(res => res.json()); + self.runInAction(() => { + self.clientInformation = ClientInformation.create({ ipAddress: answer && answer.origin }); + }); + return; + } + + self.runInAction(() => { + self.clientInformation = ClientInformation.create({ ipAddress }); + }); + }, + }; + }) + .views(self => ({ + get ipAddress() { + return self.clientInformation.ipAddress; + }, + })); + +function registerContextItems(appContext) { + appContext.clientInformationStore = ClientInformationStore.create({}, appContext); +} + +export { ClientInformationStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js new file mode 100644 index 0000000000..9f423788a7 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { sessionStore } from '@aws-ee/base-ui/dist/models/SessionStore'; + +/** + * A function that returns component's state (as MST model). + * The function creates the component's state MST model if it doesn't exist in the SessionStore. + * + * @param uiStateModel The MST model containing the component's UI state + * @param id The identifier string for the model + * @param componentStateCreatorFn The function to create the component's state MST model if it doesn't exist in the SessionStore. + * The default "componentStateCreatorFn" just uses the "create()" method of the given model to create initial state. + * + * @returns {*} + */ +function getComponentSessionState(uiStateModel, id, componentStateCreatorFn = model => model.create()) { + const stateId = `${uiStateModel.name}-${id}`; + const entry = sessionStore.get(stateId) || componentStateCreatorFn(uiStateModel); + sessionStore.set(stateId, entry); + return entry; +} + +export default getComponentSessionState; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js new file mode 100644 index 0000000000..3c8e7ff520 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import _ from 'lodash'; +import { types, getEnv, applySnapshot } from 'mobx-state-tree'; + +// This represents a specific configuration of a compute platform, such as a specific size of an ec2 setup +const ComputeConfiguration = types + .model('ComputeConfiguration', { + id: types.identifier, + type: '', + title: '', + displayOrder: types.maybe(types.number), + priceInfo: types.frozen(), + desc: '', + displayProps: types.frozen(), // an array of objects, each object has a key and a value that are purely used for displaying purposes + params: types.frozen(), + }) + .actions(self => ({ + setComputeConfiguration(raw) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, raw); + }, + })) + .views(self => ({ + get descHtml() { + const showdown = getEnv(self).showdown; + return showdown.convert(self.desc); + }, + + // Returns true if the configuration supports changing the value of a given param + isMutable(param) { + return _.has(self.params, ['mutable', param]); + }, + + // Returns all mutable parameters that this configuration allow + get mutableParams() { + return _.get(self.params, 'mutable', {}); + }, + + // If undefined is returned, it means that changing the cidr value is not supported + get defaultCidr() { + if (!self.isMutable('cidr')) return undefined; + return _.get(self.mutableParams, 'cidr', ''); + }, + + get pricePerDay() { + const info = self.priceInfo || {}; + if (info.timeUnit === 'hour') return info.value * 24; + if (info.timeUnit === 'day') return info.value; + + return undefined; + }, + + // Use this method to get a value of a parameter, regardless whether it is immutable or not + // We first see if the parameter exists in the immutable params if so, it is returned, + // otherwise the one in the mutable params is returned if any + getParam(name) { + const value = _.get(self.params, ['immutable', name]); + if (!_.isUndefined(value)) return value; + return _.get(self.mutableParams, name); + }, + })); + +export { ComputeConfiguration }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.js new file mode 100644 index 0000000000..3410501598 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.js @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import _ from 'lodash'; +import { values } from 'mobx'; +import { types, getEnv, applySnapshot } from 'mobx-state-tree'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { ComputeConfiguration } from './ComputeConfiguration'; + +// This represents a compute platform information such as an emr or an ec2 +const ComputePlatform = types + .model('ComputePlatform', { + id: types.identifier, + type: '', + title: '', + desc: '', + displayOrder: types.maybe(types.number), + configurations: types.map(ComputeConfiguration), + }) + .actions(self => ({ + setComputePlatform(rawComputePlatform) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + + // Preserve configurations + const configurations = self.configurations || {}; + applySnapshot(self, rawComputePlatform); + self.configurations = configurations; + }, + + setConfigurations(raw) { + consolidateToMap(self.configurations, raw, (exiting, newItem) => { + exiting.setComputeConfiguration(newItem); + }); + }, + })) + .views(self => ({ + get descHtml() { + const showdown = getEnv(self).showdown; + return showdown.convert(self.desc); + }, + + get configurationsList() { + return _.sortBy(values(self.configurations), 'displayOrder'); + }, + + getConfiguration(configurationId) { + return self.configurations.get(configurationId); + }, + })); + +export { ComputePlatform }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js new file mode 100644 index 0000000000..c78842f288 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import { getParent } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getComputeConfigurations } from '../../helpers/api'; + +// ================================================================== +// ComputePlatformStore +// ================================================================== +const ComputePlatformStore = BaseStore.named('ComputePlatformStore') + .props({ + platformId: '', + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const configurations = await getComputeConfigurations(self.platformId); + const platform = self.computePlatform; + if (!platform) return; + platform.setConfigurations(configurations); + }, + + cleanup: () => { + self.platformId = ''; + superCleanup(); + }, + }; + }) + + .views(self => ({ + get computePlatform() { + const parent = getParent(self, 2); + const platform = parent.getComputePlatform(self.platformId); + return platform; + }, + })); + +// Note: Do NOT register this in the app context, if you want to gain access to an instance +// use computePlatformsStore.getComputePlatformStore() +export { ComputePlatformStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js new file mode 100644 index 0000000000..cfa81cce07 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { values } from 'mobx'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { getComputePlatforms } from '../../helpers/api'; +import { ComputePlatform } from './ComputePlatform'; +import { ComputePlatformStore } from './ComputePlatformStore'; + +// ================================================================== +// ComputePlatformsStore +// ================================================================== +const ComputePlatformsStore = BaseStore.named('ComputePlatformsStore') + .props({ + platforms: types.map(ComputePlatform), + platformsStores: types.map(ComputePlatformStore), + }) + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const computePlatforms = await getComputePlatforms(); + self.runInAction(() => { + consolidateToMap(self.platforms, computePlatforms, (exiting, newItem) => { + exiting.setComputePlatform(newItem); + }); + }); + }, + + getComputePlatformStore(platformId) { + let entry = self.platformsStores.get(platformId); + if (!entry) { + // Lazily create the store + self.platformsStores.set(platformId, ComputePlatformStore.create({ platformId })); + entry = self.platformsStores.get(platformId); + } + + return entry; + }, + + cleanup() { + self.platforms.clear(); + self.platformsStores.clear(); + superCleanup(); + }, + }; + }) + .views(self => ({ + get empty() { + return self.platforms.size === 0; + }, + + get total() { + return self.platforms.size; + }, + + get list() { + return _.sortBy(values(self.platforms), 'displayOrder'); + }, + + getComputePlatform(id) { + return self.platforms.get(id); + }, + })); + +function registerContextItems(appContext) { + appContext.computePlatformsStore = ComputePlatformsStore.create({}, appContext); +} + +export { ComputePlatformsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js new file mode 100644 index 0000000000..7e187ae0f8 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const localStorageKeys = { + // Name of the id token for Data Lake APIs in local storage + appIdToken: 'appIdToken', + pinToken: 'pin', +}; + +export default localStorageKeys; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js new file mode 100644 index 0000000000..8198876c00 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js @@ -0,0 +1,134 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types, applySnapshot } from 'mobx-state-tree'; +import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore'; +import { storage } from '@aws-ee/base-ui/dist/helpers/utils'; +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +import { InstanceInfo } from './InstanceInfo'; +import { getEnvironmentKeypair, getEnvironmentNotebookUrl, getEnvironmentPasswordData } from '../../helpers/api'; +import SageMakerService from '../../helpers/sage-maker-service'; +import localStorageKeys from '../constants/local-storage-keys'; + +// ================================================================== +// Environment +// ================================================================== +const serviceCost = types.model({ + amount: types.number, + unit: types.string, +}); + +const environmentCost = types.model({ + startDate: types.string, + cost: types.map(serviceCost), +}); + +const Environment = types + .model('Environment', { + id: types.identifier, + rev: types.maybe(types.number), + description: '', + instanceInfo: types.optional(InstanceInfo, {}), + name: '', + status: '', + indexId: '', + projectId: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + costs: types.optional(types.array(environmentCost), []), + fetchingUrl: types.optional(types.boolean, false), + error: types.maybeNull(types.string), + isExternal: types.optional(types.boolean, false), + stackId: types.maybeNull(types.string), + sharedWithUsers: types.array(UserIdentifier, []), + }) + .actions(self => ({ + setEnvironment(rawEnvironment) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + + // Preserve the value of the fetchingUrl + const fetchingUrl = self.fetchingUrl; + applySnapshot(self, rawEnvironment); + self.fetchingUrl = fetchingUrl; + }, + + async getEnvironmentNotebookUrl(user) { + if (self.isExternal) { + if (!_.isEmpty(storage.getItem(localStorageKeys.pinToken))) { + const creds = await user.unencryptedCreds(storage.getItem(localStorageKeys.pinToken)); + const sm = new SageMakerService(creds.accessKeyId, creds.secretAccessKey, creds.region); + return sm.getPresignedNotebookInstanceUrl(self.instanceInfo.NotebookInstanceName); + } + throw new Error('No PIN to decrypt User credientials'); + } else { + self.setFetchingUrl(true); + return getEnvironmentNotebookUrl(self.id); + } + }, + + setFetchingUrl(value) { + self.fetchingUrl = value; + }, + + markAsTerminating() { + self.status = 'TERMINATING'; + }, + + async getKeyPair() { + return getEnvironmentKeypair(self.id, `${self.id}.pem`); + }, + + async getWindowsPassword() { + return Promise.all([self.getKeyPair(), getEnvironmentPasswordData(self.id)]); + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + get isTerminated() { + return _.includes(['TERMINATING', 'TERMINATED', 'TERMINATING_FAILED'], this.status); + }, + + get isCompleted() { + return _.includes(['COMPLETED'], this.status); + }, + + get isPending() { + return _.includes(['PENDING'], this.status); + }, + + get isError() { + return _.includes(['FAILED'], this.status); + }, + })); + +// eslint-disable-next-line no-unused-vars +function registerContextItems(appContext) { + uiEventBus.listenTo('environmentDeleted', { + id: 'Environment', + listener: async event => { + // event will be the environment object + event.markAsTerminating(); + }, + }); +} + +export { Environment, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js new file mode 100644 index 0000000000..71a95775b9 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { applySnapshot, types } from 'mobx-state-tree'; +import { getEnvironmentSpotPriceHistory } from '../../helpers/api'; + +const SpotPriceHistoryItem = types.model('SpotPriceHistoryItem', { + availabilityZone: '', + spotPrice: types.number, +}); + +const EnvironmentConfiguration = types + .model('EnvironmentConfiguration', { + id: types.identifier, + type: '', + size: '', + label: '', + price: types.number, + description: '', + defaultCidr: '', + properties: types.frozen(), + spotBidMultiplier: types.optional(types.number, 0), + spotPriceHistory: types.optional(types.array(SpotPriceHistoryItem), []), + emrConfiguration: types.frozen(), + }) + .actions(self => ({ + setEnvironmentConfiguration(configuration) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + const fetchingSpotPriceHistory = self.fetchingSpotPriceHistory; + applySnapshot(self, configuration); + self.fetchingSpotPriceHistory = fetchingSpotPriceHistory; + }, + + async getSpotPriceHistory() { + const spotInstance = self.isEmrCluster ? self.emrConfiguration.workerInstanceSize : self.size; + const prices = await getEnvironmentSpotPriceHistory(spotInstance); + + self.setSpotPriceHistory(prices); + }, + + setSpotPriceHistory(prices) { + self.spotPriceHistory = prices; + }, + })) + .views(self => ({ + get isOnDemandPricing() { + return !self.spotBidMultiplier; + }, + + get isEmrCluster() { + return !!self.emrConfiguration; + }, + + get hasSpotPriceHistory() { + return self.spotPriceHistory.length > 0; + }, + + get averageSpotPriceHistory() { + if (self.hasSpotPriceHistory) { + return self.spotPriceHistory.reduce( + (result, { spotPrice }) => result + spotPrice / self.spotPriceHistory.length, + 0, + ); + } + self.getSpotPriceHistory(); + return 0; + }, + + get spotBidPrice() { + return self.averageSpotPriceHistory * self.spotBidMultiplier; + }, + + get isLoadingPrice() { + return this.isOnDemandPricing ? false : self.averageSpotPriceHistory === 0; + }, + + get totalPrice() { + if (self.isOnDemandPricing && !self.isEmrCluster) { + return self.price * 24; + } + if (self.isOnDemandPricing && self.isEmrCluster) { + const { workerInstanceOnDemandPrice, workerInstanceCount } = self.emrConfiguration; + return (self.price + workerInstanceOnDemandPrice * workerInstanceCount) * 24; + } + // this is now a spot bid below the onDemand cost + if (self.isEmrCluster) { + const { workerInstanceCount } = self.emrConfiguration; + return (self.price + self.spotBidPrice * workerInstanceCount) * 24; + } + // last option is spot single node + return self.spotBidPrice * 24; + }, + })); + +// eslint-disable-next-line import/prefer-default-export +export { EnvironmentConfiguration }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js new file mode 100644 index 0000000000..28ff0aca57 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js @@ -0,0 +1,416 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { EnvironmentConfiguration } from './EnvironmentConfiguration'; + +const EnvironmentConfigurationsStore = BaseStore.named('EnvironmentConfigurationsStore') + .props({ + configurations: types.map(EnvironmentConfiguration), + heartbeatInterval: -1, + }) + .actions(self => { + return { + async doLoad() { + const environmentConfigurations = await getEnvironmentConfigurations(); + + self.runInAction(() => { + consolidateToMap(self.configurations, environmentConfigurations, (exiting, newItem) => { + exiting.setEnvironmentConfiguration(newItem); + }); + }); + }, + }; + }) + + .views(self => ({ + get empty() { + return self.configurations.size === 0; + }, + + get total() { + return self.configurations.size; + }, + + get list() { + const result = []; + self.configurations.forEach(configuration => result.push(configuration)); + + return _.sortBy(result, ['id']); + }, + + getConfiguration(id) { + return self.configurations.get(id); + }, + })); + +async function getEnvironmentConfigurations() { + let idCounter = 1; + return [ + { + type: 'sagemaker', + // size: 'ml.t3.xlarge', + size: 'ml.t2.medium', + label: 'Small', + price: 0.0464, + defaultCidr: '0.0.0.0/0', + description: + 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.', + properties: [ + { + key: 'vCPU', + value: '4', + }, + { + key: 'Memory (GiB)', + value: '16', + }, + ], + }, + { + type: 'sagemaker', + size: 'ml.m5.4xlarge', + label: 'Medium', + price: 1.075, + defaultCidr: '0.0.0.0/0', + description: 'A medium research workspace meant for average sized problems.', + properties: [ + { + key: 'vCPU', + value: '32', + }, + { + key: 'Memory (GiB)', + value: '128', + }, + ], + }, + { + label: 'Large', + type: 'sagemaker', + size: 'ml.m5.24xlarge', + price: 6.451, + defaultCidr: '0.0.0.0/0', + description: 'A large research workspace meant for the largest of problems. It costs the most amount per hour.', + properties: [ + { + key: 'vCPU', + value: '96', + }, + { + key: 'Memory (GiB)', + value: '384', + }, + ], + }, + { + type: 'emr', + size: 'm5.xlarge', + label: 'Small - On Demand', + price: 0.192, + defaultCidr: '', + description: + 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.', + emrConfiguration: { + workerInstanceSize: 'm5.xlarge', + workerInstanceCount: 1, + workerInstanceOnDemandPrice: 0.192, + diskSizeGb: 10, + }, + properties: [ + { + key: 'vCPU', + value: '4', + }, + { + key: 'Memory (GiB)', + value: '16', + }, + { + key: 'Worker nodes', + value: '1', + }, + ], + }, + { + type: 'emr', + size: 'm5.xlarge', + label: 'Small - Spot', + price: 0.192, + spotBidMultiplier: 1.3, + emrConfiguration: { + workerInstanceSize: 'm5.xlarge', + workerInstanceCount: 1, + workerInstanceOnDemandPrice: 0.192, + diskSizeGb: 10, + }, + defaultCidr: '', + description: + 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.', + properties: [ + { + key: 'vCPU', + value: '4', + }, + { + key: 'Memory (GiB)', + value: '16', + }, + { + key: 'Worker nodes', + value: '1', + }, + ], + }, + { + type: 'emr', + size: 'm5.xlarge', + label: 'Medium - On Demand', + price: 0.192, + emrConfiguration: { + workerInstanceSize: 'm5.xlarge', + workerInstanceCount: 8, + workerInstanceOnDemandPrice: 0.192, + diskSizeGb: 10, + }, + defaultCidr: '', + description: + 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.', + properties: [ + { + key: 'vCPU', + value: '4', + }, + { + key: 'Memory (GiB)', + value: '16', + }, + { + key: 'Worker nodes', + value: '8', + }, + ], + }, + { + type: 'emr', + size: 'm5.xlarge', + label: 'Medium - Spot', + price: 0.192, + spotBidMultiplier: 1.3, + emrConfiguration: { + workerInstanceSize: 'm5.xlarge', + workerInstanceCount: 8, + workerInstanceOnDemandPrice: 0.192, + diskSizeGb: 10, + }, + defaultCidr: '', + description: + 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.', + properties: [ + { + key: 'vCPU', + value: '4', + }, + { + key: 'Memory (GiB)', + value: '16', + }, + { + key: 'Worker nodes', + value: '8', + }, + ], + }, + { + label: 'Large - On Demand', + type: 'emr', + size: 'm5.xlarge', + price: 0.192, + emrConfiguration: { + workerInstanceSize: 'm5.24xlarge', + workerInstanceCount: 8, + workerInstanceOnDemandPrice: 4.608, + diskSizeGb: 10, + }, + defaultCidr: '', + description: + 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.', + properties: [ + { + key: 'vCPU', + value: '96', + }, + { + key: 'Memory (GiB)', + value: '384', + }, + { + key: 'Worker nodes', + value: '8', + }, + ], + }, + { + label: 'Large - Spot', + type: 'emr', + size: 'm5.xlarge', + price: 0.192, + spotBidMultiplier: 1.3, + emrConfiguration: { + workerInstanceSize: 'm5.24xlarge', + workerInstanceCount: 8, + workerInstanceOnDemandPrice: 4.608, + diskSizeGb: 10, + }, + defaultCidr: '', + description: + 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.', + properties: [ + { + key: 'vCPU', + value: '96', + }, + { + key: 'Memory (GiB)', + value: '384', + }, + { + key: 'Worker nodes', + value: '8', + }, + ], + }, + { + size: 'r5.2xlarge', + type: 'ec2-linux', + label: 'Small', + defaultCidr: '', + price: 0.504, + description: + 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.', + properties: [ + { + key: 'vCPU', + value: '8', + }, + { + key: 'Memory (GiB)', + value: '64', + }, + ], + }, + { + size: 'r5.8xlarge', + type: 'ec2-linux', + label: 'Medium', + defaultCidr: '', + price: 2.016, + description: 'A medium environment is meant for average sized problems.', + properties: [ + { + key: 'vCPU', + value: '32', + }, + { + key: 'Memory (GiB)', + value: '256', + }, + ], + }, + { + label: 'Large', + type: 'ec2-linux', + size: 'r5.16xlarge', + defaultCidr: '', + price: 4.032, + description: 'A large environment is meant for the largest of problems. It costs the most amount per hour.', + properties: [ + { + key: 'vCPU', + value: '64', + }, + { + key: 'Memory (GiB)', + value: '512', + }, + ], + }, + { + size: 'r5.2xlarge', + type: 'ec2-windows', + label: 'Small', + defaultCidr: '', + price: 0.872, + description: + 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.', + properties: [ + { + key: 'vCPU', + value: '8', + }, + { + key: 'Memory (GiB)', + value: '64', + }, + ], + }, + { + size: 'r5.8xlarge', + type: 'ec2-windows', + label: 'Medium', + defaultCidr: '', + price: 3.488, + description: 'A medium environment is meant for average sized problems.', + properties: [ + { + key: 'vCPU', + value: '32', + }, + { + key: 'Memory (GiB)', + value: '256', + }, + ], + }, + { + label: 'Large', + type: 'ec2-windows', + size: 'r5.16xlarge', + defaultCidr: '', + price: 6.976, + description: 'A large environment is meant for the largest of problems. It costs the most amount per hour.', + properties: [ + { + key: 'vCPU', + value: '64', + }, + { + key: 'Memory (GiB)', + value: '512', + }, + ], + }, + ].map(config => ({ ...config, id: `${idCounter++}` })); +} + +function registerContextItems(appContext) { + appContext.environmentConfigurationsStore = EnvironmentConfigurationsStore.create({}, appContext); +} + +export { EnvironmentConfigurationsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js new file mode 100644 index 0000000000..90f13f4d3d --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js @@ -0,0 +1,75 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { getParent } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification'; + +import { getEnvironment, getEnvironmentCost } from '../../helpers/api'; +import { getEstimatedCost } from '../../helpers/externalCostUtil'; + +// ================================================================== +// EnvironmentStore +// ================================================================== +const EnvironmentStore = BaseStore.named('EnvironmentStore') + .props({ + environmentId: '', + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const parent = getParent(self, 2); + const rawEnvironment = await getEnvironment(self.environmentId); + const envCreatedAt = new Date(rawEnvironment.createdAt); + const now = new Date(); + const diffTime = Math.abs(now - envCreatedAt); + const numberOfDaysBetweenDateCreatedAndToday = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + try { + const numberDaysInPast = Math.min(30, numberOfDaysBetweenDateCreatedAndToday); + const environmentCost = rawEnvironment.isExternal + ? await getEstimatedCost(rawEnvironment, numberDaysInPast) + : await getEnvironmentCost(self.environmentId, numberDaysInPast); + rawEnvironment.costs = environmentCost; + } catch (error) { + displayWarning('Error encountered retrieving cost data', error); + } + + parent.addEnvironment(rawEnvironment); + }, + + cleanup: () => { + superCleanup(); + }, + }; + }) + + .views(self => ({ + get environment() { + const parent = getParent(self, 2); + const w = parent.getEnvironment(self.environmentId); + return w; + }, + })); + +// Note: Do NOT register this in the global context, if you want to gain access to an instance +// use environmentsStore.getEnvironmentStore() +// eslint-disable-next-line import/prefer-default-export +export { EnvironmentStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js new file mode 100644 index 0000000000..b564ff6a2d --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js @@ -0,0 +1,306 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { getEnv, types } from 'mobx-state-tree'; +import { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification'; +import { consolidateToMap, storage } from '@aws-ee/base-ui/dist/helpers/utils'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getEstimatedCost } from '../../helpers/externalCostUtil'; +import localStorageKeys from '../constants/local-storage-keys'; +import { + getEnvironments, + deleteEnvironment, + createEnvironment, + getEnvironmentCost, + getExternalTemplate, + updateEnvironment, +} from '../../helpers/api'; +import { Environment } from './Environment'; +import { EnvironmentStore } from './EnvironmentStore'; +import CfnService from '../../helpers/cfn-service'; +import ExternalKeypairService from '../../helpers/externalKeypairService'; +import ExternalVpcService from '../../helpers/externalVpcService'; +import getExternalAccountDetails from '../../helpers/externalAccountDetails'; + +// ================================================================== +// EnvironmentsStore +// ================================================================== +const EnvironmentsStore = BaseStore.named('EnvironmentsStore') + .props({ + environments: types.optional(types.map(Environment), {}), + environmentStores: types.optional(types.map(EnvironmentStore), {}), + tickPeriod: 30 * 1000, // 30 seconds + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const environments = await getEnvironments(); + + try { + const costPromises = environments.map(env => { + if (env.isExternal) { + return getEstimatedCost(env, 1); + } + return getEnvironmentCost(env.id, 1); + }); + + const costInfo = await Promise.all(costPromises); + + for (let i = 0; i < environments.length; i++) { + environments[i].costs = costInfo[i]; + } + } catch (error) { + displayWarning('Error encountered retrieving cost data', error); + } + + self.runInAction(() => { + consolidateToMap(self.environments, environments, (exiting, newItem) => { + exiting.setEnvironment(newItem); + }); + }); + }, + + addEnvironment(rawEnvironment) { + const id = rawEnvironment.id; + const previous = self.environments.get(id); + + if (!previous) { + self.environments.put(rawEnvironment); + } else { + previous.setEnvironment(rawEnvironment); + } + }, + + getEnvironmentStore: environmentId => { + let entry = self.environmentStores.get(environmentId); + if (!entry) { + // Lazily create the store + self.environmentStores.set(environmentId, EnvironmentStore.create({ environmentId })); + entry = self.environmentStores.get(environmentId); + } + + return entry; + }, + + markAsTerminating: id => { + const previous = self.environments.get(id); + if (previous) { + previous.markAsTerminating(); + } + }, + + async updateExternalEnvironment(environment, user, pin) { + if (!environment.isExternal || !user.isExternalUser || _.isEmpty(storage.getItem(localStorageKeys.pinToken))) { + return; + } + const creds = await user.unencryptedCreds(pin); + const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region); + const request = { id: environment.id }; + try { + const response = await cfn.describeStack(environment.stackId); + _.assign(request, this.convertCfnResponse(response, environment.instanceInfo)); + } catch (e) { + _.assign(request, { status: 'FAILED', error: e.message }); + } finally { + if (environment.status !== request.status) { + updateEnvironment(request); + } + } + }, + + convertCfnResponse(response, instanceInfo) { + if (!response.isDone) { + return { status: 'PENDING' }; + } + response.outputs.forEach(output => { + _.assign(instanceInfo, { [output.key]: output.value }); + }); + instanceInfo = _.omitBy(instanceInfo, _.isEmpty); + return response.isFailed + ? { status: 'FAILED', error: response.statusReason, instanceInfo } + : response.status === 'DELETE_COMPLETE' + ? { status: 'TERMINATED', instanceInfo } + : { status: 'COMPLETED', instanceInfo }; + }, + + async deleteEnvironment(environment, user, pin) { + if (environment.isExternal) { + await this.deleteExternalEnvironment(await user.unencryptedCreds(pin), environment); + } + const uiEventBus = getEnv(self).uiEventBus; + await deleteEnvironment(environment.id); + await uiEventBus.fireEvent('environmentDeleted', environment); + }, + + async deleteExternalEnvironment(creds, environment) { + const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region); + await cfn.deleteStack(environment.stackId); + + if (environment.instanceInfo.type !== 'sagemaker') { + const externalKeypairService = new ExternalKeypairService(creds); + await externalKeypairService.delete(environment.id); + } + }, + + async createEnvironment(environment) { + // environment = { platformId, configurationId, name, description, projectId, studyIds, params, pin } + // - projectId is only available if the user is not external + // - pin is only available if creation of the environment is done by an external researcher user role. + // and should never be sent to the server + const user = self.user; + const result = user.isExternalResearcher + ? await this.createExternalEnvironment( + await user.unencryptedCreds(environment.pin), + user.username, + _.omit(environment, ['pin']), // remove the pin, we don't want to send it to the server + ) + : await createEnvironment(environment); + self.addEnvironment(result); + return self.getEnvironment(result.id); + }, + + async createExternalEnvironment(creds, username, rawEnvironment) { + const { platformId, configurationId } = rawEnvironment; + const configuration = self.getComputeConfiguration(platformId, configurationId); + const { type, title } = configuration; + const size = configuration.getParam('size'); + + // We need to get the external account details to pass the account Id to the api to allow ami access + const { Account: accountId } = await getExternalAccountDetails(creds); + // We first call the backend because it will enrich with id and the imageId if needed + const environment = await createEnvironment({ ...rawEnvironment, accountId }); + const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region); + const name = `analysis-${new Date().getTime()}`; + const params = await this.getExternalParams({ environment, name, creds }); + const url = await getExternalTemplate(`${type}.cfn.yml`); + const response = await cfn.createStack( + name, + params, + url, + username, + `Created By ${username} - ${title} - ${type} - ${size}`, + ); + + return updateEnvironment({ id: environment.id, stackId: response.StackId }); + }, + + async getExternalParams({ + environment: { + id, + instanceInfo: { type, size, config, cidr, s3Mounts, iamPolicyDocument, environmentInstanceFiles }, + amiImage, + }, + name, + creds, + }) { + const cfnParams = []; + const addParam = (key, v) => cfnParams.push({ ParameterKey: key, ParameterValue: `${v}` }); + + addParam('Namespace', name); + addParam('S3Mounts', s3Mounts); + addParam('IamPolicyDocument', iamPolicyDocument); + addParam('EnvironmentInstanceFiles', environmentInstanceFiles); + + const externalVpcService = new ExternalVpcService(creds); + const { vpcId, subnetId } = await externalVpcService.defaultVPCInfo(); + addParam('VPC', vpcId); + addParam('Subnet', subnetId); + + if (type === 'sagemaker') { + addParam('InstanceType', size); // Yes, size here is actually the instance type we want to send to cfn + } + + if (type === 'emr') { + addParam('DiskSizeGB', config.diskSizeGb.toString()); + addParam('MasterInstanceType', size); + addParam('WorkerInstanceType', config.workerInstanceSize); + addParam('CoreNodeCount', config.workerInstanceCount.toString()); + + // Add parameters to support spot instance pricing if specified + // TODO this needs to be parameterized + const isOnDemand = !config.spotBidPrice; + // The spot bid price can only have 3 decimal places maximum + const spotBidPrice = isOnDemand ? '0' : config.spotBidPrice.toFixed(3); + + addParam('Market', isOnDemand ? 'ON_DEMAND' : 'SPOT'); + addParam('WorkerBidPrice', spotBidPrice); + + // These paramaters apply for types apart from sagemaker, but keep the logic simple for now + const externalKeypairService = new ExternalKeypairService(creds); + const keyName = await externalKeypairService.create(id); + + addParam('AmiId', amiImage); + addParam('AccessFromCIDRBlock', cidr); + addParam('KeyName', keyName); + } + + return cfnParams; + }, + + async updateEnvironment(environment) { + await updateEnvironment(environment); + }, + + cleanup: () => { + storage.removeItem(localStorageKeys.pinToken); + self.environments.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get empty() { + return self.environments.size === 0; + }, + + get total() { + return self.environments.size; + }, + + get list() { + const result = []; + self.environments.forEach(environment => result.push(environment)); + + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + getEnvironment(id) { + return self.environments.get(id); + }, + + get user() { + return getEnv(self).userStore.user; + }, + + getComputeConfiguration(platformId, configurationId) { + const store = getEnv(self).computePlatformsStore; + const platform = store.getComputePlatform(platformId); + if (!platform) return undefined; + return platform.getConfiguration(configurationId); + }, + })); + +function registerContextItems(appContext) { + appContext.environmentsStore = EnvironmentsStore.create({}, appContext); +} + +export { EnvironmentsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js new file mode 100644 index 0000000000..df67aa917c --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { getSnapshot, types } from 'mobx-state-tree'; + +const InstanceInfo = types + .model('InstanceInfo', { + Ec2WorkspaceDnsName: '', + type: '', + size: '', + JupyterUrl: '', + NotebookInstanceName: '', + s3Mounts: '', + iamPolicyDocument: '', + environmentInstanceFiles: '', + }) + .views(self => ({ + get id() { + return self.identifierStr; + }, + get identifier() { + return self; + }, + get identifierStr() { + return JSON.stringify(getSnapshot(self)); + }, + })); + +// eslint-disable-next-line import/prefer-default-export +export { InstanceInfo }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js new file mode 100644 index 0000000000..5515b1e031 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +import uuidv4 from 'uuid/v4'; + +const FileUpload = types + .model('FileUpload', { + id: types.identifier, + status: types.union( + types.literal('PENDING'), + types.literal('UPLOADING'), + types.literal('COMPLETE'), + types.literal('FAILED'), + ), + uploaded: types.maybeNull(types.number), + error: types.maybeNull(types.string), + }) + .volatile(() => ({ + file: undefined, + cancel: undefined, + })) + .views(self => ({ + get size() { + return self.file ? self.file.size : 0; + }, + get name() { + return self.file ? self.file.name : ''; + }, + getFile() { + return self.file; + }, + })) + .actions(self => ({ + updateProgress(uploadedBytes) { + self.uploaded = uploadedBytes; + }, + updateStatusToUploading() { + self.status = 'UPLOADING'; + }, + updateStatusToComplete() { + self.status = 'COMPLETE'; + }, + updateStatusToFailed(error) { + self.status = 'FAILED'; + self.error = error; + }, + setFile(file) { + self.file = file; + }, + setCancel(cancel) { + self.cancel = cancel; + }, + doCancel() { + if (self.cancel) { + self.cancel(); + self.cancel = undefined; + } + }, + })); + +const FileUploadGroup = types + .model('FileUploadGroup', { + resourceId: types.identifier, + fileUploads: types.map(FileUpload), + state: types.union(types.literal('PENDING'), types.literal('UPLOADING'), types.literal('COMPLETE')), + }) + .views(self => ({ + get fileUploadObjects() { + return Array.from(self.fileUploads.values()); + }, + getFileUpload(fileUploadId) { + return self.fileUploads.get(fileUploadId); + }, + })) + .actions(self => ({ + async start(fileUploadHandler) { + if (self.state !== 'PENDING') { + throw new Error(`Cannot transition state from ${self.state} -> UPLOADING`); + } + self.setStateToUploading(); + const fileUploads = Array.from(self.fileUploads.values()).filter(fileUpload => fileUpload.status === 'PENDING'); + await Promise.all( + fileUploads.map(async fileUpload => { + fileUpload.updateStatusToUploading(); + try { + await fileUploadHandler(fileUpload); + } catch (error) { + fileUpload.updateStatusToFailed(error.message); + } + }), + ); + self.setStateToComplete(); + }, + cancel() { + self.fileUploads.forEach(fileUpload => { + fileUpload.doCancel(); + }); + }, + remove(id) { + self.fileUploads.delete(id); + }, + add({ file }) { + const model = FileUpload.create({ + id: uuidv4(), + status: 'PENDING', + }); + model.setFile(file); + self.fileUploads.put(model); + }, + setStateToComplete() { + self.state = 'COMPLETE'; + }, + setStateToUploading() { + self.state = 'UPLOADING'; + }, + })); + +export default FileUploadGroup; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js new file mode 100644 index 0000000000..d7ec72a5a0 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +import FileUploadGroup from './FileUploadGroup'; + +const FileUploadsStore = types + .model('FileUploadsStore', { + fileUploadGroups: types.map(FileUploadGroup), + }) + .actions(self => ({ + getFileUploadGroup(resourceId) { + let group = self.fileUploadGroups.get(resourceId); + if (!group) { + group = FileUploadGroup.create({ resourceId, state: 'PENDING' }); + self.fileUploadGroups.put(group); + } + return group; + }, + resetFileUploadGroup(resourceId) { + const group = FileUploadGroup.create({ resourceId, state: 'PENDING' }); + self.fileUploadGroups.put(group); + }, + })); + +function registerContextItems(appContext) { + appContext.fileUploadsStore = FileUploadsStore.create({}, appContext); +} + +export { FileUploadsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js new file mode 100644 index 0000000000..f57a2a7540 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const addAwsAccountFormFields = { + name: { + label: 'Account Name', + placeholder: 'Type the name of this account', + rules: 'required|string|between:1,100', + }, + accountId: { + label: 'AWS Account ID', + placeholder: 'Type the 12-digit AWS account ID', + rules: 'required|string|size:12', + }, + roleArn: { + label: 'Role Arn', + placeholder: 'Type Role ARN for launching resources into this AWS account', + rules: 'required|string|between:10,300', + }, + externalId: { + label: 'External ID', + placeholder: 'Type external ID for this AWS account', + rules: 'required|string|between:1,300', + }, + description: { + label: 'Description', + placeholder: 'Type description for this AWS account', + rules: 'required|string', + }, + vpcId: { + label: 'VPC ID', + placeholder: 'Type the ID of the VPC where EMR clusters will be launched', + rules: 'required|string|min:12|max:21', + }, + subnetId: { + label: 'Subnet ID', + placeholder: 'Type the ID of the subnet where the EMR clusters will be launched', + rules: 'required|string|min:15|max:24', + }, + encryptionKeyArn: { + label: 'KMS Encryption Key ARN', + placeholder: 'Type the KMS Encryption Key ARN to use for this AWS account', + rules: 'required|string|between:1,100', + }, +}; + +function getAddAwsAccountFormFields() { + return addAwsAccountFormFields; +} + +function getAddAwsAccountForm() { + return createForm(addAwsAccountFormFields); +} + +export { getAddAwsAccountFormFields, getAddAwsAccountForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js new file mode 100644 index 0000000000..a1aae5f39d --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const addIndexFormFields = { + id: { + label: 'Index ID', + placeholder: 'Type id for this index', + rules: 'required|string|between:1,300', + }, + awsAccountId: { + label: 'AWS Account ID', + }, + description: { + label: 'Description', + placeholder: 'Type description for this index', + rules: 'string|between:1,3000', + }, +}; + +function getAddIndexFormFields() { + return addIndexFormFields; +} + +function getAddIndexForm() { + return createForm(addIndexFormFields); +} + +export { getAddIndexFormFields, getAddIndexForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js new file mode 100644 index 0000000000..5e4af10087 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js @@ -0,0 +1,53 @@ +/* eslint-disable import/prefer-default-export */ +import { createForm } from '../../helpers/form'; + +const addUserFormFields = { + email: { + label: 'Username', + placeholder: 'Type email address as username for the user', + extra: { explain: 'Username in email format' }, + rules: 'required|email|string', + }, + password: { + label: 'Password', + placeholder: 'Type default password for the user', + explain: `The password must be between 4 and 2048 characters long.`, + rules: 'required|string|between:4,2048', + }, + firstName: { + label: 'First Name', + placeholder: 'Type first name of the user', + rules: 'required|string|between:1,500', + }, + lastName: { + label: 'Last Name', + placeholder: 'Type last name of the user', + rules: 'required|string|between:1,500', + }, + userRole: { + label: 'UserRole', + extra: { explain: "Select user's role" }, + rules: 'required', + }, + projectId: { + label: 'Projects', + extra: { explain: 'Select projects that this user are associated with' }, + }, + status: { + label: 'Status', + extra: { + explain: 'Active users can log into the Research Portal', + yesLabel: 'Active', + noLabel: 'Inactive', + yesValue: 'active', + noValue: 'inactive', + }, + rules: 'required', + }, +}; + +function getAddUserForm() { + return createForm(addUserFormFields); +} + +export { getAddUserForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js new file mode 100644 index 0000000000..69a8015156 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const addProjectFormFields = { + id: { + label: 'Project ID', + placeholder: 'Type id for this project', + rules: 'required|string|between:1,300', + }, + indexId: { + label: 'Index ID', + }, + description: { + label: 'Description', + placeholder: 'Type description for this project', + rules: 'string|between:1,3000', + }, +}; + +function getAddProjectFormFields() { + return addProjectFormFields; +} + +function getAddProjectForm() { + return createForm(addProjectFormFields); +} + +export { getAddProjectFormFields, getAddProjectForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.js new file mode 100644 index 0000000000..f046c2983e --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.js @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const addUserApplicationFormFields = { + email: { + label: 'Username', + placeholder: 'Type email address as username for the user', + }, + firstName: { + label: 'First Name', + placeholder: 'Your first name', + explain: 'Your current default first name is ', + rules: 'required|string', + }, + lastName: { + label: 'Last Name', + placeholder: 'Your last name', + explain: 'Your current default last name is ', + rules: 'required|string', + }, + applyReason: { + label: 'Describe Your Research', + explain: 'Please tell us why you are requesting access to the Research Portal', + placeholder: 'Why are you requesting access?', + rules: 'required|string', + }, +}; + +function getAddUserApplicationFormFields() { + return addUserApplicationFormFields; +} + +function getAddUserApplicationForm() { + return createForm(addUserApplicationFormFields); +} + +export { getAddUserApplicationFormFields, getAddUserApplicationForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js new file mode 100644 index 0000000000..a8e2f675f8 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const addUserFormFields = { + email: { + label: 'Username', + placeholder: 'Type email address as username for the user', + extra: { explain: 'Username in email format' }, + rules: 'required|email|string', + }, + identityProviderName: { + label: 'Identity Provider', + extra: { explain: 'Identity Provider for this user' }, + }, + projectId: { + label: 'Project Id', + extra: { explain: 'Select Project for this user' }, + }, + userRole: { + label: 'UserRole', + extra: { explain: "Select user's role" }, + }, + status: { + label: 'Status', + extra: { + explain: 'Active users can log into the Research Portal', + yesLabel: 'Active', + noLabel: 'Inactive', + yesValue: 'active', + noValue: 'inactive', + }, + }, +}; + +function getAddUserFormFields() { + return addUserFormFields; +} + +function getAddUserForm() { + return createForm(addUserFormFields); +} + +export { getAddUserFormFields, getAddUserForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js new file mode 100644 index 0000000000..3a10d1feae --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +const createAwsAccountFormFields = { + accountName: { + label: 'Account Name', + placeholder: 'Type the name of this account', + rules: 'required|string|between:1,100', + }, + accountEmail: { + label: 'AWS Account Email', + placeholder: 'Type AWS account email', + rules: 'required|string|email', + }, + masterRoleArn: { + label: 'Master Role Arn', + placeholder: 'Type configured Role ARN of master account of the Organization', + rules: 'required|string|between:10,300', + }, + externalId: { + label: 'External ID', + placeholder: 'Type external ID for this AWS account', + rules: 'required|string|between:1,300', + }, + description: { + label: 'Description', + placeholder: 'Type description for this AWS account', + rules: 'required|string', + }, +}; + +function getCreateAwsAccountFormFields() { + return createAwsAccountFormFields; +} + +function getCreateAwsAccountForm() { + return createForm(createAwsAccountFormFields); +} + +export { getCreateAwsAccountFormFields, getCreateAwsAccountForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js new file mode 100644 index 0000000000..8d5168bbb6 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; + +import { createForm } from '../../helpers/form'; + +const getFields = ({ askForCredentials, cidr }) => { + const rulesPrefix = askForCredentials ? 'required|' : ''; + const fields = { + name: { + label: 'Name', + placeholder: 'Type a name for this research workspace', + extra: { + explain: + 'Name can contain only alphanumeric characters (case sensitive) and hyphens. It must start with an alphabetic character and cannot be longer than 128 characters', + }, + rules: 'required|string|between:3,128|regex:/^[A-Za-z][A-Za-z0-9-]+$/', + }, + description: { + label: 'Description', + placeholder: 'The description of this research workspace', + rules: 'required|string|between:3,2048', + }, + configurationId: { + label: 'Configuration', + placeholder: 'The configuration for the research workspace', + rules: 'required', + }, + accessKeyId: { + label: 'IAM Access Key Id', + placeholder: 'Access key for your IAM user', + rules: `${rulesPrefix}string|between:16,128`, + }, + secretAccessKey: { + label: 'IAM Secret Access Key', + placeholder: 'Secret access key for your IAM user', + rules: `${rulesPrefix}string|size:40`, // TODO - is this right? + }, + pin: { + label: 'PIN', + placeholder: 'A PIN or password to secure your IAM credentials', + rules: 'required|string|between:4,16', + }, + }; + + if (!_.isUndefined(cidr)) { + fields.cidr = { + label: 'Whitelisted CIDR', + extra: { + explain: `This research workspace will only be reachable from this CIDR. You can get your organization's CIDR range from your IT department. The provided default is the CIDR that restricts to your IP address.`, + }, + placeholder: 'The CIDR range to restrict research workspace access to', + rules: 'required|cidr', + value: cidr, + }; + } + + return fields; +}; + +function getCreateExternalPlatformForm(...args) { + return createForm(getFields(...args)); +} + +// eslint-disable-next-line import/prefer-default-export +export { getCreateExternalPlatformForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js new file mode 100644 index 0000000000..67b05b705e --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { createForm } from '../../helpers/form'; + +const getFields = ({ projectIdOptions, cidr }) => { + const fields = { + name: { + label: 'Name', + placeholder: 'Type a name for this research workspace', + extra: { + explain: + 'Name can contain only alphanumeric characters (case sensitive) and hyphens. It must start with an alphabetic character and cannot be longer than 128 characters', + }, + rules: 'required|string|between:3,128|regex:/^[A-Za-z][A-Za-z0-9-]+$/', + }, + description: { + label: 'Description', + placeholder: 'The description of this research workspace', + rules: 'required|string|between:3,2048', + }, + projectId: { + label: 'Project ID', + placeholder: 'The project ID associated with this study', + rules: ['required', 'string', 'min:1', 'max:100'], + extra: { + options: projectIdOptions, + }, + }, + configurationId: { + label: 'Configuration', + placeholder: 'The configuration for the research workspace', + rules: 'required', + }, + }; + + if (!_.isUndefined(cidr)) { + fields.cidr = { + label: 'Whitelisted CIDR', + extra: { + explain: `This research workspace will only be reachable from this CIDR. You can get your organization's CIDR range from your IT department. The provided default is the CIDR that restricts to your IP address.`, + }, + placeholder: 'The CIDR range to restrict research workspace access to', + rules: 'required|cidr', + value: cidr, + }; + } + + return fields; +}; + +function getCreateInternalPlatformForm(...args) { + return createForm(getFields(...args)); +} + +// eslint-disable-next-line import/prefer-default-export +export { getCreateInternalPlatformForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js new file mode 100644 index 0000000000..8ad930e4f4 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; +import { categories } from '../studies/categories'; + +const createStudyFields = { + // General fields + id: { + label: 'ID', + placeholder: 'A unique ID used to reference the study', + extra: { + explain: 'Must be less than 100 characters long and only contain alphanumeric characters, "-", or "_"', + }, + rules: ['required', 'string', 'between:1,100', 'regex:/^[A-Za-z0-9-_]+$/'], + }, + categoryId: { + label: '', // not shown because extra.showHeader = false + extra: { + explain: + 'If you choose "My Study", only you can access it. If you choose "Organization Study", you get to decide who can access it.', + yesLabel: 'My Study', + noLabel: 'Organization Study', + yesValue: categories.myStudies.id, + noValue: categories.organization.id, + showHeader: false, + }, + rules: ['required'], + }, + name: { + label: 'Name', + placeholder: 'A name for the study', + rules: ['string', 'max:2048'], + }, + description: { + label: 'Description', + placeholder: 'A description of the study', + rules: ['string', 'max:8192'], + }, + projectId: { + label: 'Project ID', + placeholder: 'The project ID associated with this study', + rules: ['required', 'string', 'min:1', 'max:100'], + }, +}; + +const getCreateStudyForm = () => { + return createForm(createStudyFields); +}; + +export { getCreateStudyForm }; // eslint-disable-line import/prefer-default-export diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js new file mode 100644 index 0000000000..519f956194 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +function getIAMFormFields(isIAMRequired = true) { + const rulesPrefix = isIAMRequired ? 'required|' : ''; + return [ + { + name: 'accessKeyId', + label: 'IAM User Access Key Id', + placeholder: 'Access Key for your IAM user', + rules: `${rulesPrefix}string|size:20`, + }, + { + name: 'secretAccessKey', + label: 'IAM User Secret Access Key', + placeholder: 'Secret access Key for your IAM user', + rules: `${rulesPrefix}string|size:40`, + }, + { + name: 'region', + label: 'AWS Region', + placeholder: 'us-east-1', + rules: `${rulesPrefix}string|regex:/^[a-z]{2}(-gov)?-[a-z]*-\\d$/`, + }, + { + name: 'pin', + label: 'PIN', + placeholder: 'A PIN or password to secure your IAM credentials', + rules: 'required|string|between:4,16', + }, + ]; +} + +function getExternalUserIAMForm() { + return createForm(getIAMFormFields()); +} + +export { getExternalUserIAMForm, getIAMFormFields }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.js new file mode 100644 index 0000000000..b13a0a3c8b --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.js @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { createForm } from '../../helpers/form'; + +function getExternalUserPinFormFields() { + return [ + { + name: 'pin', + label: 'PIN', + placeholder: 'A PIN or password to secure your IAM credentials', + rules: 'required|string|between:4,16', + }, + ]; +} + +function getExternalUserPinForm() { + return createForm(getExternalUserPinFormFields()); +} + +export { getExternalUserPinForm, getExternalUserPinFormFields }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js new file mode 100644 index 0000000000..e4f6d1b5b4 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js @@ -0,0 +1,82 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { createForm } from '../../helpers/form'; +import { toValueFromIdp } from './UserFormUtils'; + +function getUpdateUserConfigFormFields(existingUser) { + return { + username: { + label: 'Username', + value: _.get(existingUser, 'username', ''), + }, + firstName: { + label: 'First Name', + placeholder: 'First name of this user', + value: _.get(existingUser, 'firstName', ''), + rules: 'required|string', + }, + lastName: { + label: 'Last Name', + placeholder: 'Last name of this user', + value: _.get(existingUser, 'lastName', ''), + rules: 'required|string', + }, + email: { + label: 'Email', + placeholder: 'email address', + value: _.get(existingUser, 'email', ''), + rules: 'required|email|string', + }, + identityProviderName: { + label: 'Identity Provider Name', + value: toValueFromIdp({ + authenticationProviderId: _.get(existingUser, 'authenticationProviderId', ''), + identityProviderName: _.get(existingUser, 'identityProviderName', ''), + }), + }, + projectId: { + label: 'Project', + value: _.get(existingUser, 'projectId', ''), + }, + userRole: { + label: 'User Role', + value: _.get(existingUser, 'userRole', ''), + }, + applyReason: { + label: 'Reason for Applying', + explain: ' ', + value: _.get(existingUser, 'applyReason', ''), + }, + status: { + label: 'User Status', + extra: { + explain: 'Active users can log into the Research Portal', + yesLabel: 'Active', + noLabel: 'Inactive', + yesValue: 'active', + noValue: 'inactive', + }, + value: _.get(existingUser, 'status', ''), + }, + }; +} + +function getUpdateUserConfigForm(existingUser) { + return createForm(getUpdateUserConfigFormFields(existingUser)); +} + +export { getUpdateUserConfigFormFields, getUpdateUserConfigForm }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.js new file mode 100644 index 0000000000..71384227fc --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.js @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; + +/** + * Returns identity provider options that can be used for displaying idp selection options + * @param providerConfigs An array of authentication provider configuration objects. For details about the shape of + * the object see "authenticationProviderConfigs" property of + * "addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigsStore.js" + * + * @returns {[]} + */ +function toIdpOptions(providerConfigs) { + const options = []; + + _.forEach(providerConfigs, providerConfig => { + const config = providerConfig.config; + + // Each providerConfig (authentication provider) can have zero or more identity providers. + if (!_.isEmpty(config.federatedIdentityProviders)) { + _.forEach(config.federatedIdentityProviders, idp => { + options.push({ + key: idp.id, + text: idp.name, + + // Make sure the authentication provider's information is embedded in the value + // along with the idp name. This is required so disambiguate two idps with the same idp name based on which + // authentication provider they belong to + value: JSON.stringify({ authNProviderId: providerConfig.id, idpName: idp.name }), + }); + }); + } + }); + return options; +} + +// From string to object +function toIdpFromValue(value) { + return JSON.parse(value); +} + +// From object to string +function toValueFromIdp({ authenticationProviderId, identityProviderName }) { + return JSON.stringify({ authNProviderId: authenticationProviderId, idpName: identityProviderName }); +} + +// eslint-disable-next-line import/prefer-default-export +export { toIdpOptions, toIdpFromValue, toValueFromIdp }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js new file mode 100644 index 0000000000..76056ac31b --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types, applySnapshot } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +// ================================================================== +// Index +// ================================================================== +const Index = types + .model('Index', { + id: types.identifier, + rev: types.maybe(types.number), + awsAccountId: '', + description: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .actions(self => ({ + setIndex(rawIndex) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, rawIndex); + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + })); + +// eslint-disable-next-line import/prefer-default-export +export { Index }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js new file mode 100644 index 0000000000..2a406aaa1e --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js @@ -0,0 +1,124 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; +import { getIndexes, addIndex } from '../../helpers/api'; +import { Index } from './Index'; + +// ================================================================== +// IndexesStore +// ================================================================== +const IndexesStore = BaseStore.named('IndexesStore') + .props({ + indexes: types.optional(types.map(Index), {}), + tickPeriod: 900 * 1000, // 15 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const indexes = (await getIndexes()) || []; + self.runInAction(() => { + consolidateToMap(self.indexes, indexes, (exiting, newItem) => { + exiting.setIndex(newItem); + }); + }); + }, + + addIndex: async index => { + const addedIndex = await addIndex(index); + self.runInAction(() => { + // Added newly created user to users map + const addedIndexModel = Index.create(addedIndex); + self.indexes.set(addedIndexModel.id, addedIndexModel); + }); + }, + + getIndexesStore: indexesId => { + let entry = self.indexesStores.get(indexesId); + if (!entry) { + // Lazily create the store + self.indexesStores.set(indexesId, IndexesStore.create({ indexesId })); + entry = self.indexesStores.get(indexesId); + } + + return entry; + }, + + getIndex: indexesId => { + let res = {}; + self.indexes.forEach(index => { + if (index.id === indexesId) res = _.clone(index); + }); + return res; + }, + + cleanup: () => { + self.indexes.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get dropdownOptions() { + const result = []; + // converting map self.users to result array + self.indexes.forEach(index => { + const proj = {}; + proj.key = index.id; + proj.value = index.id; + proj.text = index.id; + result.push(proj); + }); + return result; + }, + + get empty() { + return self.indexes.size === 0; + }, + + get total() { + return self.indexes.size; + }, + + get list() { + const result = []; + self.indexes.forEach(indexes => result.push(indexes)); + + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + hasIndexes(id) { + return self.indexes.has(id); + }, + + getIndexes(id) { + return self.indexes.get(id); + }, + })); + +function registerContextItems(appContext) { + appContext.indexesStore = IndexesStore.create({}, appContext); +} + +export { IndexesStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js new file mode 100644 index 0000000000..872d31e408 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types, applySnapshot } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +// ================================================================== +// Project +// ================================================================== +const Project = types + .model('Project', { + id: types.identifier, + rev: types.maybe(types.number), + description: '', + indexId: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .actions(self => ({ + setProject(rawProject) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, rawProject); + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + })); + +// eslint-disable-next-line import/prefer-default-export +export { Project }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js new file mode 100644 index 0000000000..402a5a00a5 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { getParent } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getProject } from '../../helpers/api'; + +// ================================================================== +// ProjectStore +// ================================================================== +const ProjectStore = BaseStore.named('ProjectStore') + .props({ + projectId: '', + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const parent = getParent(self, 2); + const rawProject = await getProject(self.projectId); + parent.addProject(rawProject); + }, + + cleanup: () => { + superCleanup(); + }, + }; + }) + + .views(self => ({ + get project() { + const parent = getParent(self, 2); + const w = parent.getProject(self.projectId); + return w; + }, + })); + +// Note: Do NOT register this in the global context, if you want to gain access to an instance +// use projectsStore.getProjectStore() +// eslint-disable-next-line import/prefer-default-export +export { ProjectStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js new file mode 100644 index 0000000000..81e1669ad5 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js @@ -0,0 +1,124 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { getProjects, addProject } from '../../helpers/api'; +import { Project } from './Project'; +import { ProjectStore } from './ProjectStore'; + +// ================================================================== +// ProjectsStore +// ================================================================== +const ProjectsStore = BaseStore.named('ProjectsStore') + .props({ + projects: types.optional(types.map(Project), {}), + projectStores: types.optional(types.map(ProjectStore), {}), + tickPeriod: 900 * 1000, // 15 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const projects = await getProjects(); + // We try to preserve existing projects data and merge the new data instead + // We could have used self.projects.replace(), but it will do clear() then merge() + self.runInAction(() => { + consolidateToMap(self.projects, projects, (exiting, newItem) => { + exiting.setProject(newItem); + }); + }); + }, + + async addProject(rawProject) { + const id = rawProject.id; + const previous = self.projects.get(id); + + if (!previous) { + self.projects.put(rawProject); + await addProject(rawProject); + } else { + previous.setProject(rawProject); + } + }, + + getProjectStore: projectId => { + let entry = self.projectStores.get(projectId); + if (!entry) { + // Lazily create the store + self.projectStores.set(projectId, ProjectStore.create({ projectId })); + entry = self.projectStores.get(projectId); + } + + return entry; + }, + + cleanup: () => { + self.projects.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get empty() { + return self.projects.size === 0; + }, + + get total() { + return self.projects.size; + }, + + get list() { + const result = []; + self.projects.forEach(project => result.push(project)); + + return _.reverse(_.sortBy(result, ['createdAt', 'id'])); + }, + + get dropdownOptions() { + const result = []; + // converting map self.users to result array + self.projects.forEach(project => { + const res = {}; + res.key = project.id; + res.value = project.id; + res.text = project.id; + result.push(res); + }); + + return result; + }, + + hasProject(id) { + return self.projects.has(id); + }, + + getProject(id) { + return self.projects.get(id); + }, + })); + +function registerContextItems(appContext) { + appContext.projectsStore = ProjectsStore.create({}, appContext); +} + +export { ProjectsStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js new file mode 100644 index 0000000000..a0520d9178 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +// import { createSession } from '../../helpers/api'; + +const File = types.model('File', { + name: '', // the extension of the file determines its type such as cram or crai + size: types.optional(types.number, 0), +}); + +const Run = types.model('Run', { + id: '', + sample: '', + alignment: '', + sex: '', + center: '', + release: '', + files: types.optional(types.array(File), []), +}); + +const Consent = types.model('Consent', { + id: '', // such as 'phs00001.v1.p1.c1 + name: '', // 'code --- qualifier' + runs: types.optional(types.array(Run), []), +}); + +const Token = types.model('Token', { + id: '', + expireAt: '', + sessionId: '', + username: '', +}); + +const Study = types.model('Study', { + id: '', + name: '', + consents: types.optional(types.array(Consent), []), +}); + +const Session = types.model('Session', { + id: types.identifier, + title: '', + studies: types.optional(types.array(Study), []), + tokens: types.optional(types.array(Token), []), +}); + +// eslint-disable-next-line no-unused-vars +function createNewSession(raw) { + return ( + Promise.resolve() + // .then(() => createSession(raw)) + .then(result => Session.create(result)) + ); +} + +export { Session, createNewSession }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js new file mode 100644 index 0000000000..d81e02adbf --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js @@ -0,0 +1,135 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types, applySnapshot } from 'mobx-state-tree'; + +// TODO: Improve file model +// const File2 = types.model('File2', { +// name: '', +// size: types.optional(types.number, 0), +// }); + +// TODO this should have been named 'Run' +const File = types.model('File', { + id: types.identifier, + name: '', + description: types.maybeNull(types.string), + accessStatus: '', +}); + +// TODO this should have been named 'RunsSelection' +const FilesSelection = types + .model('FilesSelection', { + files: types.optional(types.map(File), {}), + }) + .actions(self => ({ + setFile(file) { + self.files.set(file.id, file); + }, + deleteFile(id) { + self.files.delete(id); + }, + cleanup() { + self.files.clear(); + }, + setFiles(filesMapSnapshot) { + applySnapshot(self.files, filesMapSnapshot); + }, + })) + .views(self => ({ + hasFile(id) { + return self.files.has(id); + }, + get empty() { + return self.files.size === 0; + }, + get count() { + return self.files.size; + }, + get studiesCount() { + const studyIdMap = {}; + self.files.forEach(entry => { + studyIdMap[entry.studyId] = true; + }); + + return _.size(studyIdMap); + }, + studiesCountByStatus: state => { + const studyIdMap = {}; + self.files.forEach(entry => { + if (entry.accessStatus === state) studyIdMap[entry.studyId] = true; + }); + + return _.size(studyIdMap); + }, + studiesCountByNotStatus: state => { + const studyIdMap = {}; + self.files.forEach(entry => { + if (entry.accessStatus !== state) studyIdMap[entry.studyId] = true; + }); + + return _.size(studyIdMap); + }, + get fileNames() { + const names = []; + self.files.forEach(entry => { + names.push(entry.id); + }); + + return names; + }, + groupByStudy: () => { + const studyIdMap = {}; + self.files.forEach(entry => { + const values = studyIdMap[entry.studyId]; + if (_.isArray(values)) { + values.push(entry); + } else { + studyIdMap[entry.studyId] = [entry]; + } + }); + + return studyIdMap; + }, + groupNotApprovedByStudy: () => { + const studyIdMap = {}; + self.files.forEach(entry => { + if (entry.accessStatus === 'approved') return; + const values = studyIdMap[entry.studyId]; + if (_.isArray(values)) { + values.push(entry); + } else { + studyIdMap[entry.studyId] = [entry]; + } + }); + + return studyIdMap; + }, + countByStatus: state => { + let counter = 0; + self.files.forEach(file => { + if (file.accessStatus === state) counter += 1; + }); + + return counter; + }, + })); + +function registerContextItems(appContext) { + appContext.filesSelection = FilesSelection.create({}, appContext); +} + +export { FilesSelection, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js new file mode 100644 index 0000000000..05c69766ee --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js @@ -0,0 +1,127 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; + +import { getStudies, createStudy } from '../../helpers/api'; +import { categories } from './categories'; +import { Study } from './Study'; +import { StudyStore } from './StudyStore'; + +// ================================================================== +// StudiesStore +// ================================================================== +const StudiesStore = BaseStore.named('StudiesStore') + .props({ + category: '', + studies: types.optional(types.map(Study), {}), + studyStores: types.optional(types.map(StudyStore), {}), + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const studies = await getStudies(self.category); + // We try to preserve existing studies data and merge the new data instead + // We could have used self.studies.replace(), but it will do clear() then merge() + self.runInAction(() => { + consolidateToMap(self.studies, studies, (exiting, newItem) => { + exiting.setStudy(newItem); + }); + }); + }, + + addStudy(rawStudy) { + const id = rawStudy.id; + const previous = self.studies.get(id); + + if (!previous) { + self.studies.put(rawStudy); + } else { + previous.setStudy(rawStudy); + } + }, + + getStudyStore: studyId => { + let entry = self.studyStores.get(studyId); + if (!entry) { + // Lazily create the store + self.studyStores.set(studyId, StudyStore.create({ studyId })); + entry = self.studyStores.get(studyId); + } + + return entry; + }, + + async createStudy(study) { + const result = await createStudy({ ...study, uploadLocationEnabled: true }); + self.runInAction(() => { + self.addStudy(result); + }); + const resultStudy = self.getStudy(result.id); + + return resultStudy; + }, + + cleanup: () => { + self.studies.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get empty() { + return self.studies.size === 0; + }, + + get total() { + return self.studies.size; + }, + + get list() { + const result = []; + self.studies.forEach(study => result.push(study)); + + return _.reverse(_.sortBy(result, ['createdAt', 'name'])); + }, + + hasStudy(id) { + return self.studies.has(id); + }, + + getStudy(id) { + return self.studies.get(id); + }, + })); + +function registerContextItems(appContext) { + appContext.studiesStoresMap = { + // TODO - we should be using ids when calling the backend but the backend needs fixing too since it does not support ids yet + [categories.myStudies.id]: StudiesStore.create({ category: categories.myStudies.name }), + [categories.organization.id]: StudiesStore.create({ category: categories.organization.name }), + [categories.openData.id]: StudiesStore.create({ category: categories.openData.name }), + }; +} + +export { registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js new file mode 100644 index 0000000000..1fd24be105 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types, applySnapshot } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +import { StudyFilesStore } from './StudyFilesStore'; +import { StudyPermissionsStore } from './StudyPermissionsStore'; +import { categories } from './categories'; + +// ================================================================== +// Study +// ================================================================== +const Study = types + .model('Study', { + id: types.identifier, + rev: types.maybe(types.number), + name: '', + category: '', + projectId: '', + access: types.maybe(types.string), + resources: types.optional(types.array(types.model({ arn: types.string })), []), + description: types.maybeNull(types.string), + uploadLocationEnabled: false, + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + filesStore: types.maybe(StudyFilesStore), + permissionsStore: types.maybe(StudyPermissionsStore), + }) + .actions(self => ({ + setStudy(rawStudy) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, rawStudy); + }, + + getFilesStore() { + if (!self.filesStore) { + self.filesStore = StudyFilesStore.create({ studyId: self.id }); + } + return self.filesStore; + }, + + getPermissionsStore() { + if (!self.permissionsStore) { + self.permissionsStore = StudyPermissionsStore.create({ studyId: self.id }); + } + return self.permissionsStore; + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + get isOpenDataStudy() { + return self.category === categories.openData.name; // TODO the backend should really send an id and not a name + }, + + get isOrganizationStudy() { + return self.category === categories.organization.name; // TODO the backend should really send an id and not a name + }, + })); + +export { Study }; // eslint-disable-line import/prefer-default-export diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js new file mode 100644 index 0000000000..904921f879 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js @@ -0,0 +1,87 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { listStudyFiles } from '../../helpers/api'; + +// ================================================================== +// StudyFile +// ================================================================== +const StudyFile = types.model('StudyFile', { + filename: types.string, + size: types.integer, + lastModified: types.Date, +}); + +// ================================================================== +// StudyFiles +// ================================================================== +const StudyFilesStore = BaseStore.named('StudyFilesStore') + .props({ + studyId: '', + files: types.array(StudyFile), + tickPeriod: 5 * 1000, // 5 seconds + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + // Retrieve files + let files = await listStudyFiles(self.studyId); + + // Determine which files were added or removed + const comparator = (fileA, fileB) => fileA.filename === fileB.filename; + const removed = _.differenceWith(self.files, files, comparator); + const added = _.differenceWith(files, self.files, comparator); + + // Only update store when needed to avoid unnecessary re-rendering + if (removed.length !== 0 || added.length !== 0) { + // Sort files by name and cast lastModified as Date() + files = files + .sort((fileA, fileB) => fileA.filename.localeCompare(fileB.filename)) + .map(file => ({ + ...file, + lastModified: new Date(file.lastModified), + })); + + // Update store + self.runInAction(() => { + self.files.replace(files); + }); + } + }, + + cleanup: () => { + self.files.clear(); + superCleanup(); + }, + }; + }) + + .views(self => ({ + get empty() { + return self.files.length === 0; + }, + })); + +// Note: Do NOT register this in the global context, if you want to gain access to an instance +// use study.getFilesStore() +export { StudyFilesStore }; // eslint-disable-line import/prefer-default-export diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js new file mode 100644 index 0000000000..2a401f263f --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import { types } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +// ================================================================== +// Study Permissions +// ================================================================== +const StudyPermissions = types + .model('StudyPermissions', { + id: types.identifier, + adminUsers: types.array(UserIdentifier), + readonlyUsers: types.array(UserIdentifier), + createdAt: '', + createdBy: UserIdentifier, + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .views(_self => ({ + get userTypes() { + return ['admin', 'readonly']; + }, + })); + +export { StudyPermissions }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js new file mode 100644 index 0000000000..8934dc93ce --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/* eslint-disable import/prefer-default-export */ +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getStudyPermissions, updateStudyPermissions } from '../../helpers/api'; +import { StudyPermissions } from './StudyPermissions'; + +// ================================================================== +// StudyStore +// ================================================================== +const StudyPermissionsStore = BaseStore.named('StudyPermissionsStore') + .props({ + studyId: types.identifier, + studyPermissions: types.maybe(StudyPermissions), + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + doLoad: async () => { + const newPermissions = await getStudyPermissions(self.studyId); + if (!self.studyPermissions || !_.isEqual(self.studyPermissions, newPermissions)) { + self.runInAction(() => { + self.studyPermissions = newPermissions; + }); + } + }, + + cleanup: () => { + superCleanup(); + }, + + update: async selectedUsers => { + const updateRequest = { usersToAdd: [], usersToRemove: [] }; + + self.studyPermissions.userTypes.forEach(type => { + const userToRequestFormat = user => ({ principalIdentifier: user, permissionLevel: type }); + + // Set selected users as "usersToAdd" (API is idempotent) + updateRequest.usersToAdd.push(...selectedUsers[type].map(userToRequestFormat)); + + // Set removed users as "usersToRemove" + updateRequest.usersToRemove.push( + ..._.differenceWith(self.studyPermissions[`${type}Users`], selectedUsers[type], _.isEqual).map( + userToRequestFormat, + ), + ); + }); + + // Perform update and reload store + await updateStudyPermissions(self.studyId, updateRequest); + await self.load(); + }, + }; + }); + +export { StudyPermissionsStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js new file mode 100644 index 0000000000..dc01377bed --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { getParent } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { getStudy } from '../../helpers/api'; + +// ================================================================== +// StudyStore +// ================================================================== +const StudyStore = BaseStore.named('StudyStore') + .props({ + studyId: '', + tickPeriod: 300 * 1000, // 5 minutes + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const parent = getParent(self, 2); + const rawStudy = await getStudy(self.studyId); + parent.addStudy(rawStudy); + }, + + cleanup: () => { + superCleanup(); + }, + }; + }) + + .views(self => ({ + get study() { + const parent = getParent(self, 2); + const w = parent.getStudy(self.studyId); + return w; + }, + })); + +// Note: Do NOT register this in the global context, if you want to gain access to an instance +// use studiesStore.getStudyStore() +// eslint-disable-next-line import/prefer-default-export +export { StudyStore }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js new file mode 100644 index 0000000000..5417116307 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; + +const categories = { + myStudies: { name: 'My Studies', id: 'my-studies' }, + organization: { name: 'Organization', id: 'organization' }, + openData: { name: 'Open Data', id: 'open-data' }, +}; + +function getCategoryById(id) { + return _.find(categories, ['id', id]); +} + +export { categories, getCategoryById }; // eslint-disable-line import/prefer-default-export diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js new file mode 100644 index 0000000000..8759c38932 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types, applySnapshot } from 'mobx-state-tree'; + +import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier'; + +// ================================================================== +// UserRole +// ================================================================== +const UserRole = types + .model('UserRole', { + id: types.identifier, + rev: types.maybe(types.number), + description: '', + userType: '', + createdAt: '', + createdBy: types.optional(UserIdentifier, {}), + updatedAt: '', + updatedBy: types.optional(UserIdentifier, {}), + }) + .actions(self => ({ + setUserRole(rawUserRole) { + // Note: if you have partial data vs full data, you need to replace the applySnapshot() with + // the appropriate logic + applySnapshot(self, rawUserRole); + }, + })) + + // eslint-disable-next-line no-unused-vars + .views(self => ({ + // add view methods here + })); + +// eslint-disable-next-line import/prefer-default-export +export { UserRole }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js new file mode 100644 index 0000000000..d3f78ada76 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils'; +import { getUserRoles } from '../../helpers/api'; +import { UserRole } from './UserRole'; + +// ================================================================== +// UserRolesStore +// ================================================================== +const UserRolesStore = BaseStore.named('UserRolesStore') + .props({ + userRoles: types.optional(types.map(UserRole), {}), + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const userRoles = (await getUserRoles()) || []; + self.runInAction(() => { + consolidateToMap(self.userRoles, userRoles, (exiting, newItem) => { + exiting.setUserRole(newItem); + }); + }); + }, + + cleanup: () => { + superCleanup(); + }, + }; + }) + + .views(self => ({ + get list() { + const result = []; + // converting map self.users to result array + self.userRoles.forEach(userRole => result.push(userRole)); + return result; + }, + get dropdownOptions() { + const result = []; + // converting map self.users to result array + self.userRoles.forEach(userRole => { + const role = {}; + role.key = userRole.id; + role.value = userRole.id; + role.text = userRole.id; + result.push(role); + }); + return result; + }, + + isInternalUser(userRoleId) { + return _.toLower(self.getUserType(userRoleId)) === 'internal'; + }, + + isInternalGuest(userRoleId) { + return _.toLower(userRoleId) === 'internal-guest'; + }, + + getUserType(userRoleId) { + const found = self.userRoles.get(userRoleId); + return found ? found.userType : ''; + }, + })); + +function registerContextItems(appContext) { + appContext.userRolesStore = UserRolesStore.create({}, appContext); +} + +export { UserRolesStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js new file mode 100644 index 0000000000..c41808b29a --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js @@ -0,0 +1,195 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { types } from 'mobx-state-tree'; +import _ from 'lodash'; +import { storage, removeNulls } from '@aws-ee/base-ui/dist/helpers/utils'; +import { aesGcmEncrypt, aesGcmDecrypt } from '../../helpers/crypto'; +import localStorageKeys from '../constants/local-storage-keys'; + +const User = types + .model('User', { + firstName: types.maybeNull(types.optional(types.string, '')), + lastName: types.maybeNull(types.optional(types.string, '')), + isAdmin: types.optional(types.boolean, false), + username: '', + ns: types.maybeNull(types.optional(types.string, '')), + email: '', + userType: '', + authenticationProviderId: '', // Id of the authentication provider this user is authenticated against (such as internal, cognito auth provider id etc) + identityProviderName: types.maybeNull(types.optional(types.string, '')), // Name of the identity provider this user belongs to (such as Identity Provider Id in cognito user pool in case of Federation etc) + status: 'active', + rev: 0, + userRole: '', + projectId: types.array(types.string, []), // TODO this property should be named projectIds + isExternalUser: types.optional(types.boolean, false), // TODO we need to consider have this a derived property + encryptedCreds: types.maybeNull(types.string), + applyReason: '', + }) + .actions(self => ({ + runInAction(fn) { + return fn(); + }, + async setEncryptedCreds(unencryptedCreds, pin) { + unencryptedCreds.region = unencryptedCreds.region || 'us-east-1'; + const encryptedCreds = await aesGcmEncrypt(JSON.stringify(unencryptedCreds), pin); + // TODO Should we store the pin in the session? + storage.setItem(localStorageKeys.pinToken, pin); + self.runInAction(() => { + self.encryptedCreds = encryptedCreds; + }); + }, + async clearEncryptedCreds() { + self.runInAction(() => { + self.encryptedCreds = undefined; + }); + }, + setUser(rawUser) { + removeNulls(rawUser); + self.firstName = rawUser.firstName || self.firstName || ''; + self.lastName = rawUser.lastName || self.lastName || ''; + self.isAdmin = rawUser.isAdmin || self.isAdmin; + self.isExternalUser = rawUser.isExternalUser || self.isExternalUser; + self.username = rawUser.username || self.username; + self.ns = rawUser.ns || self.ns; + self.email = rawUser.email || self.email; + self.authenticationProviderId = rawUser.authenticationProviderId || self.authenticationProviderId; + self.identityProviderName = rawUser.identityProviderName || self.identityProviderName; + self.status = rawUser.status || self.status || 'active'; + self.createdBy = rawUser.createdBy || self.createdBy; + self.rev = rawUser.rev || self.rev || 0; + self.userRole = rawUser.userRole || self.userRole; + self.projectId = rawUser.projectId || self.projectId || []; + self.encryptedCreds = rawUser.encryptedCreds || self.encryptedCreds; + self.applyReason = rawUser.applyReason || self.applyReason || ''; + // we don't update the other fields because they are being populated by a separate store + }, + })) + .views(self => ({ + get displayName() { + return `${self.firstName}`; + }, + + get longDisplayName() { + if (self.unknown) { + return `${self.username}??`; + } + const fullName = `${self.firstName} ${self.lastName}`; + if (self.email) { + return `${fullName} (${self.email})`; + } + return fullName; + }, + + get unknown() { + return !self.firstName && !self.lastName; + }, + + get isRootUser() { + return _.toLower(self.userType) === 'root'; + }, + + get isActive() { + return _.toLower(self.status) === 'active'; + }, + + get isInternalGuest() { + return self.userRole === 'internal-guest'; + }, + + get isExternalGuest() { + return self.userRole === 'guest'; + }, + + get isInternalResearcher() { + return self.userRole === 'researcher'; + }, + + get isExternalResearcher() { + return self.userRole === 'external-researcher'; + }, + + get isSystem() { + const identifier = self.identifier; + return identifier.username === '_system_'; + }, + + isSame({ username, ns }) { + return self.username === username && self.ns === ns; + }, + + get id() { + return self.identifierStr; + }, + + get identifier() { + return { username: self.username, ns: self.ns }; + }, + + get identifierStr() { + return JSON.stringify(self.identifier); + }, + + get hasProjects() { + return !_.isEmpty(self.projectId); + }, + + get hasCredentials() { + return self.isExternalResearcher && !_.isEmpty(self.encryptedCreds) && self.encryptedCreds !== 'N/A'; + }, + + // TODO - this should not be a view, it should be moved to the actions section + // - a better approach is to do unencryptedCreds as a view but then + // have the call to store the pin in a separate method that is in the action + async unencryptedCreds(pin) { + try { + const creds = JSON.parse(await aesGcmDecrypt(self.encryptedCreds, pin)); + // TODO Should we store the pin in the session? + storage.setItem(localStorageKeys.pinToken, pin); + return creds; + } catch (e) { + throw new Error('Invalid PIN. Please try again'); + } + }, + + // A map of high level actions that the user is allowed to perform. + // Example: { 'canCreateStudy': true/false, 'canCreateWorkspace': true/false } + // + // Note: actions that require a resource before the permission is determined, are NOT captured in this capability matrix. + get capabilities() { + const active = self.isActive; + const external = self.isExternalUser; // Either external guest or external user + const externalGuest = self.isExternalGuest; + const internalGuest = self.isInternalGuest; + + const canCreateStudy = active && !external && !internalGuest; + const canCreateWorkspace = active && !externalGuest && !internalGuest; + const canSelectStudy = active && !externalGuest && !internalGuest; + const canViewDashboard = active && !external && !internalGuest; + + return { + canCreateStudy, + canCreateWorkspace, + canSelectStudy, + canViewDashboard, + }; + }, + })); + +function getIdentifierObjFromId(identifierStr) { + return JSON.parse(identifierStr); +} + +export { User, getIdentifierObjFromId }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js new file mode 100644 index 0000000000..6a64c56095 --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js @@ -0,0 +1,93 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { types } from 'mobx-state-tree'; + +import { getUser } from '@aws-ee/base-ui/dist/helpers/api'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { User } from './User'; + +const UserStore = BaseStore.named('UserStore') + .props({ + user: types.maybe(User), + }) + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const user = await getUser(); + self.runInAction(() => { + self.user = User.create(user); + }); + }, + cleanup: () => { + self.user = undefined; + superCleanup(); + }, + }; + }) + + .views(self => ({ + get empty() { + return _.isEmpty(self.user); + }, + + // TODO this method should really be moved to the User model and renamed to something like projectIdOptions + get projectIdDropdown() { + const result = _.map(self.user.projectId, id => ({ key: id, value: id, text: id })); + return result; + }, + + get cloneUser() { + let result = {}; + const { + username, + authenticationProviderId, + identityProviderName, + firstName, + lastName, + email, + isAdmin, + status, + userRole, + rev, + projectId, + } = self.user; + result = { + username, + authenticationProviderId, + identityProviderName, + firstName, + lastName, + email, + isAdmin, + status, + rev, + userRole, + projectId, + }; + return result; + }, + })); + +function registerContextItems(appContext) { + appContext.userStore = UserStore.create({}, appContext); +} + +export { UserStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js new file mode 100644 index 0000000000..bb516567fc --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js @@ -0,0 +1,236 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import _ from 'lodash'; +import { applySnapshot, detach, getSnapshot, types } from 'mobx-state-tree'; +import { addUser, updateUser, getUsers } from '@aws-ee/base-ui/dist/helpers/api'; +import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; + +import { deleteUser, addUsers, updateUserApplication } from '../../helpers/api'; +import { User } from './User'; + +const UsersStore = BaseStore.named('UsersStore') + .props({ + users: types.optional(types.map(User), {}), + tickPeriod: 60 * 1000, // 1 minute + }) + + .actions(self => { + // save the base implementation of cleanup + const superCleanup = self.cleanup; + + return { + async doLoad() { + const users = (await getUsers()) || []; + self.runInAction(() => { + users.forEach(user => { + const userModel = User.create(user); + const previous = self.users.get(userModel.id); + if (!previous) { + self.users.set(userModel.id, userModel); + } else { + applySnapshot(previous, user); + } + }); + }); + }, + + cleanup: () => { + self.users.clear(); + superCleanup(); + }, + addUser: async user => { + // if username is not specified then pass email as username + const username = user.username || user.email; + const addedUser = await addUser({ ...user, username }); + self.runInAction(() => { + // Added newly created user to users map + const addedUserModel = User.create(addedUser); + self.users.set(addedUserModel.id, addedUserModel); + }); + }, + addUsers: async users => { + await addUsers(users); + }, + updateUser: async user => { + const updatedUser = await updateUser(user); + const userModel = User.create(updatedUser); + const previousUser = self.users.get(userModel.id); + applySnapshot(previousUser, updatedUser); + }, + updateUserApplication: async user => { + const res = await updateUserApplication(user); + return res; + }, + deleteUser: async user => { + const id = user && user.id ? user.id : User.create(user).id; + await deleteUser(user); + const deletedUser = self.users.get(id); + self.runInAction(() => { + // Detaching here instead of deleting because the ReactTable component in UsersList somehow still fires + // "Cell" component rendering after the user is deleted from the map + // That results in the following error + // "You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'User', Path upon death: " + detach(deletedUser); + // self.users.delete(id); + }); + // return deletedUser; + }, + }; + }) + + .views(self => ({ + get empty() { + return self.users.size === 0; + }, + + get hasNonRootAdmins() { + const nonRootAdmins = _.filter(self.list, user => user.isAdmin && !user.isRootUser); + return !_.isEmpty(nonRootAdmins); + }, + + get hasNonRootUsers() { + return !_.isEmpty(self.nonRootUsers); + }, + + get nonRootUsers() { + return _.filter(self.list, user => !user.isRootUser); + }, + + get list() { + const result = []; + // converting map self.users to result array + self.users.forEach(user => result.push(user)); + return result; + }, + + asSelectOptions({ nonClearables = [] } = {}) { + const result = []; + self.users.forEach(user => + result.push({ + value: user.id, + label: user.longDisplayName, + clearableValue: !nonClearables.includes(user.id), + }), + ); + return result; + }, + + asDropDownOptions({ status = 'active' } = {}) { + const result = []; + self.users.forEach(user => { + if (user.status === status) { + result.push({ + key: user.id, + value: user.id, + text: user.longDisplayName, + }); + } + }); + return result; + }, + + asUserObject(userIdentifier) { + if (userIdentifier) { + const user = self.users.get(userIdentifier.id); + return user || User.create({ username: userIdentifier.username, ns: userIdentifier.ns }); // this could happen in the employee is no longer active or with the company + } + return undefined; + }, + + asUserObjects(userIdentifiers = []) { + const result = []; + userIdentifiers.forEach(userIdentifier => { + if (userIdentifier) { + const user = self.users.get(userIdentifier.id); + if (user) { + result.push(user); + } else { + result.push(User.create(getSnapshot(userIdentifier))); + } // this could happen in the employee is no longer active or with the company + } + }); + + return result; + }, + })); + +// function registerModels(globals) { +// globals.usersStore = UsersStore.create({}, globals); +// } + +// export { UsersStore, toUserIds, toLongNames, toLongName, registerModels }; + +// const UsersStore = BaseUsersStore.named('UsersStore') +// .actions(self => { +// return { +// addUser: async user => { +// const addedUser = await addUser(user); +// self.runInAction(() => { +// // Added newly created user to users map +// const addedUserModel = User.create(addedUser); +// self.users.set(addedUserModel.id, addedUserModel); +// }); +// }, +// updateUser: async user => { +// const updatedUser = await updateUser(user); +// const userModel = User.create(updatedUser); +// const previousUser = self.users.get(userModel.id); +// applySnapshot(previousUser, updatedUser); +// }, +// addUsers: async users => { +// await addUsers(users); +// }, +// updateUserApplication: async user => { +// const res = await updateUserApplication(user); +// return res; +// }, +// deleteUser: async user => { +// await deleteUser(user); +// }, +// }; +// }) + +// .views(self => ({ +// asUserObject(userIdentifier) { +// if (userIdentifier) { +// const user = self.users.get(userIdentifier.id); +// return user || User.create({ username: userIdentifier.username, ns: userIdentifier.ns }); +// } +// return undefined; +// }, + +// asUserObjects(userIdentifiers = []) { +// const result = []; +// userIdentifiers.forEach(userIdentifier => { +// if (userIdentifier) { +// const user = self.users.get(userIdentifier.id); +// if (user) { +// result.push(user); +// } else { +// result.push(User.create(getSnapshot(userIdentifier))); +// } +// } +// }); + +// return result; +// }, +// })); + +function registerContextItems(appContext) { + appContext.usersStore = UsersStore.create({}, appContext); +} + +export { UsersStore, registerContextItems }; diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js new file mode 100644 index 0000000000..0addb655ee --- /dev/null +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js @@ -0,0 +1,165 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import { withRouter } from 'react-router-dom'; +import { decorate, observable, action, runInAction } from 'mobx'; +import { Button, Header, Container, Message } from 'semantic-ui-react'; +import { displayError } from '@aws-ee/base-ui/dist/helpers/notification'; +import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form'; +import Input from '@aws-ee/base-ui/dist/parts/helpers/fields/Input'; +import TextArea from '@aws-ee/base-ui/dist/parts/helpers/fields/TextArea'; + +import { getAddUserApplicationForm } from '../models/forms/AddUserApplicationForm'; + +class UserApplication extends React.Component { + constructor(props) { + super(props); + this.state = {}; + + runInAction(() => { + this.logoutProcessing = false; + this.currentUser = props.userStore.cloneUser; + }); + this.form = getAddUserApplicationForm(); + this.form.$('email').value = this.currentUser.username; + } + + handleLogout = action(async event => { + this.logoutProcessing = true; + event.preventDefault(); + event.stopPropagation(); + + try { + await this.props.authentication.logout(); + runInAction(() => { + this.logoutProcessing = false; + }); + } catch (error) { + displayError(error); + runInAction(() => { + this.logoutProcessing = false; + }); + } + }); + + render() { + let content = null; + if (this.currentUser.status === 'pending') { + content = this.renderFormSubmittedMessage(); + } else { + content = this.renderAddUserPage(); + } + return content; + } + + renderAddUserPage() { + return ( + +
+
+ Research Portal Application +
+
{this.renderAddUserForm()}
+
+
+ ); + } + + renderFormSubmittedMessage() { + const processing = this.logoutProcessing; + return ( + + + + We have received your application + You will not have access to the portal until an administrator reviews and approves your application. +
+ We recommend you logout and login when you have received access. + +
+
+
+
+ ); + } + + renderAddUserForm() { + const form = this.form; + + return ( +
+ {({ processing }) => ( + <> + + + +