From c96825a6f4c9290c32472e0e7c4280bb1ca1707e Mon Sep 17 00:00:00 2001 From: Samantha Date: Wed, 10 Jan 2024 15:22:28 -0500 Subject: [PATCH 1/5] [ALS-5539] Parameterize user register TOS acceptance (#159) * Update register page to enable/disable required TOS. * Parameterize the TOS link enable/disable on the landing page. --- .../packages/base-ui/src/helpers/settings.js | 4 ++ .../main/lib/schemas/register-user.json | 2 +- .../packages/main/src/extend/withAuth.js | 8 ++- .../packages/main/src/parts/Register.js | 68 +++++++++++-------- main/config/settings/example.yml | 6 ++ .../config/infra/functions.yml | 2 + .../config/settings/.defaults.yml | 6 ++ .../ui/config/environment/env-template.yml | 2 + .../solution/ui/config/settings/.defaults.yml | 6 ++ 9 files changed, 72 insertions(+), 32 deletions(-) diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js index 0717f6c9f4..6b7219642c 100644 --- a/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js +++ b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js @@ -30,6 +30,10 @@ const branding = { title: process.env.REACT_APP_USER_REGISTRATION_TITLE, summary: process.env.REACT_APP_USER_REGISTRATION_SUMMARY, success: process.env.REACT_APP_USER_REGISTRATION_SUCCESS, + tosRequired: process.env.REACT_APP_USER_REGISTRATION_TOS_REQUIRED === 'true', + }, + tos: { + onLanding: process.env.REACT_APP_TOS_LINK_ON_LANDING === 'true', }, main: { title: process.env.REACT_APP_BRAND_MAIN_TITLE, diff --git a/addons/addon-custom/packages/main/lib/schemas/register-user.json b/addons/addon-custom/packages/main/lib/schemas/register-user.json index 64da161c45..fea1d1406b 100644 --- a/addons/addon-custom/packages/main/lib/schemas/register-user.json +++ b/addons/addon-custom/packages/main/lib/schemas/register-user.json @@ -27,5 +27,5 @@ "pattern": "\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z)" } }, - "required": ["email", "firstName", "lastName", "acceptedTerms"] + "required": ["email", "firstName", "lastName"] } diff --git a/addons/addon-custom/packages/main/src/extend/withAuth.js b/addons/addon-custom/packages/main/src/extend/withAuth.js index bd87259472..07aadb689f 100644 --- a/addons/addon-custom/packages/main/src/extend/withAuth.js +++ b/addons/addon-custom/packages/main/src/extend/withAuth.js @@ -56,8 +56,12 @@ function RegisterLogin(enableCustomRegister) { Register )} - Terms of Service -
+ {branding.tos.onLanding && ( + <> + Terms of Service +
+ + )} {branding.main.loginWarning} ); diff --git a/addons/addon-custom/packages/main/src/parts/Register.js b/addons/addon-custom/packages/main/src/parts/Register.js index 77f73a1248..053fe40f9c 100644 --- a/addons/addon-custom/packages/main/src/parts/Register.js +++ b/addons/addon-custom/packages/main/src/parts/Register.js @@ -83,6 +83,32 @@ class Register extends React.Component { }; } + renderTOS() { + return ( + <> + {this.terms.value !== termsState.unset.value && } + {this.terms.label}   + { + this.termsModalButton = ref; + }} + > + Terms of Service + + } + closeOnDimmerClick + acceptAction={this.setTerms(termsState.accepted)} + declineAction={this.setTerms(termsState.declined)} + /> + + ); + } + renderRegisterationForm() { return (
@@ -101,28 +127,8 @@ class Register extends React.Component { {this.renderField('email')} + {branding.register.tosRequired &&
{this.renderTOS()}
}
- {this.terms.value !== termsState.unset.value && } - {this.terms.label}   - { - this.termsModalButton = ref; - }} - > - Terms of Service - - } - closeOnDimmerClick - acceptAction={this.setTerms(termsState.accepted)} - declineAction={this.setTerms(termsState.declined)} - /> -
-
{this.errors.form && ( @@ -130,11 +136,7 @@ class Register extends React.Component {
)} - + Create a new Service Workbench account @@ -145,6 +147,10 @@ class Register extends React.Component { ); } + submitDisabled() { + return branding.register.tosRequired ? this.terms.value !== termsState.accepted.value : false; + } + renderConfirmation() { return (
@@ -207,20 +213,24 @@ class Register extends React.Component { return; } - // Validate that the terms have been accepted - if (this.terms.value !== termsState.accepted.value) { + // Validate that the terms have been accepted, if required + let acceptedTerms; + if (branding.register.tosRequired && this.terms.value !== termsState.accepted.value) { runInAction(() => { this.errors.form = termsErrorText; this.formProcessing = false; }); return; } + if (branding.register.tosRequired) { + acceptedTerms = new Date().toISOString(); + } const result = await registerUser({ firstName: this.user.firstName, lastName: this.user.lastName, email: this.user.email, - acceptedTerms: new Date().toISOString(), + acceptedTerms, }); // if we encounter an error then don't continue to process the form and instead display a message if (result.error) { diff --git a/main/config/settings/example.yml b/main/config/settings/example.yml index 022684a95e..e4d85b2cfa 100644 --- a/main/config/settings/example.yml +++ b/main/config/settings/example.yml @@ -157,5 +157,11 @@ HostedZoneId: 'Z02455261RJ9QQPVHFZGA' #userRegistrationSuccess: "

Your Service Workbench account has been successfully created. What you should expect next:

  1. The Service Workbench administrator will review your account.
  2. Once your account is activated, you can login to Service Workbench and start your research.
" #loginWarning: "WARNING: You are entering a secure environment." +# Require the TOS to be accepted before registration can occur. +#tosRequired: true + +# Should the TOS link be enabled on the landing page. +#tosLinkOnLanding: true + # URL for help link #helpUrl: about:blank \ No newline at end of file diff --git a/main/solution/post-deployment/config/infra/functions.yml b/main/solution/post-deployment/config/infra/functions.yml index 6b871858e4..4bb1012f2f 100644 --- a/main/solution/post-deployment/config/infra/functions.yml +++ b/main/solution/post-deployment/config/infra/functions.yml @@ -42,6 +42,8 @@ postDeployment: APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName} APP_ENABLE_NATIVE_USER_POOL_USERS: ${self:custom.settings.enableNativeUserPoolUsers} APP_ENABLE_CUSTOM_REGISTRATION: ${self:custom.settings.enableCustomRegistration} + APP_TOS_LINK_ON_LANDING : ${self:custom.settings.tosLinkOnLanding} + APP_USER_REGISTRATION_TOS_REQUIRED: ${self:custom.settings.tosRequired} APP_AUTO_CONFIRM_NATIVE_USERS: ${self:custom.settings.autoConfirmNativeUsers} APP_NATIVE_ADMIN_PASSWORD_PARAM_NAME: ${self:custom.settings.nativeAdminPasswordParamName} APP_ENV_NAME: ${self:custom.settings.envName} diff --git a/main/solution/post-deployment/config/settings/.defaults.yml b/main/solution/post-deployment/config/settings/.defaults.yml index eed64ce4dd..9f6cc110ff 100644 --- a/main/solution/post-deployment/config/settings/.defaults.yml +++ b/main/solution/post-deployment/config/settings/.defaults.yml @@ -36,6 +36,12 @@ enableUserSignUps: true # Please also include the registration parameters to customize the registration page. enableCustomRegistration: false +# Require the TOS to be accepted before registration can occur. +tosRequired: true + +# Should the TOS link be enabled on the landing page. +tosLinkOnLanding: true + # Cognito domain prefix. Note random string will be padded at the end if specified domain is not available cognitoUserPoolDomainPrefix: ${self:custom.settings.envName}-${self:custom.settings.solutionName} diff --git a/main/solution/ui/config/environment/env-template.yml b/main/solution/ui/config/environment/env-template.yml index 08e1f7cfd0..a4dd23453b 100644 --- a/main/solution/ui/config/environment/env-template.yml +++ b/main/solution/ui/config/environment/env-template.yml @@ -26,6 +26,8 @@ REACT_APP_USER_REGISTRATION_SUMMARY: ${self:custom.settings.userRegistrationSumm REACT_APP_USER_REGISTRATION_SUCCESS: ${self:custom.settings.userRegistrationSuccess} REACT_APP_LOGIN_WARNING: ${self:custom.settings.loginWarning} REACT_APP_HELP_URL: ${self:custom.settings.helpUrl} +REACT_APP_USER_REGISTRATION_TOS_REQUIRED: ${self:custom.settings.tosRequired} +REACT_APP_TOS_LINK_ON_LANDING: ${self:custom.settings.tosLinkOnLanding} # ======================================================================== # Overrides for .env.local diff --git a/main/solution/ui/config/settings/.defaults.yml b/main/solution/ui/config/settings/.defaults.yml index f8ca55d08c..6aad9fa6f1 100644 --- a/main/solution/ui/config/settings/.defaults.yml +++ b/main/solution/ui/config/settings/.defaults.yml @@ -34,3 +34,9 @@ userRegistrationTitle: "WELCOME TO SERVICE WORKBENCH" userRegistrationSummary: "

Service Workbench provides a self-service, three-click, on-demand service for researchers to build research environments in minutes without needing cloud infrastructure knowledge. Fill out the form below to create your account on Service Workbench hosted on AWS.

" userRegistrationSuccess: "

Your Service Workbench account has been successfully created. What you should expect next:

  1. The Service Workbench administrator will review your account.
  2. Once your account is activated, you can login to Service Workbench and start your research.
" loginWarning: "" + +# Require the TOS to be accepted before registration can occur. +tosRequired: true + +# Should the TOS link be enabled on the landing page. +tosLinkOnLanding: true From d5559f0cde46ce3b0759609ba69819477e807f5c Mon Sep 17 00:00:00 2001 From: Samantha Date: Tue, 23 Jan 2024 11:19:26 -0500 Subject: [PATCH 2/5] [ALS-5632] Add comma separated study whitelist in stage file (#160) * Add comma seperated study whitelist in stage file. --- .../backend/config/infra/functions.yml | 1 + .../backend/config/settings/.defaults.yml | 3 + .../__tests__/handler-impl.test.js | 58 +++++++++++++++++-- .../open-data-scrape-handler/handler-impl.js | 20 +++++-- .../open-data-scrape-handler/handler.js | 3 +- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/main/solution/backend/config/infra/functions.yml b/main/solution/backend/config/infra/functions.yml index d4ab37c470..40ac19bb1a 100644 --- a/main/solution/backend/config/infra/functions.yml +++ b/main/solution/backend/config/infra/functions.yml @@ -20,6 +20,7 @@ openDataScrapeHandler: APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName} APP_CUSTOM_USER_AGENT: ${self:custom.settings.customUserAgent} APP_IS_APP_STREAM_ENABLED: ${self:custom.settings.isAppStreamEnabled} + APP_OPEN_DATA_SCRAPE_WHITELIST: ${self:custom.settings.openDataScrapeWhitelist} events: - schedule: diff --git a/main/solution/backend/config/settings/.defaults.yml b/main/solution/backend/config/settings/.defaults.yml index 61426bcf28..6518ccc720 100644 --- a/main/solution/backend/config/settings/.defaults.yml +++ b/main/solution/backend/config/settings/.defaults.yml @@ -321,3 +321,6 @@ dataSourceReachabilityHandlerRoleArn: 'arn:aws:iam::${self:custom.settings.awsAc # The stack name of the 'backend' serverless service backendStackName: ${self:custom.settings.namespace}-backend + +# custom list of open studies to include when scraping that are not part in the filtered tag list +openDataScrapeWhitelist: '' \ No newline at end of file diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/__tests__/handler-impl.test.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/__tests__/handler-impl.test.js index dbd2a0b108..7334656cb3 100644 --- a/main/solution/backend/src/lambdas/open-data-scrape-handler/__tests__/handler-impl.test.js +++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/__tests__/handler-impl.test.js @@ -113,33 +113,83 @@ describe('fetchOpenData', () => { sha: 'abc2', }; + const whitelistStudy = { + name: 'Study 3', + description: 'Example study 3', + tags: ['other-tag'], + resources: [ + { + description: 'Description for Study 3', + arn: 'arn:aws:s3:::study3', + region: 'us-east-1', + type: 'S3 Bucket', + }, + ], + id: 'study-3', + sha: 'abc2', + }; + it('has invalid study (Invalid ARN)', async () => { const fileUrls = ['firstFileUrl']; const requiredTags = ['genetic']; + const studyWhitelist = []; const fetchFile = jest.fn(); fetchFile.mockReturnValueOnce(invalidStudy); - const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile }); + const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile }); expect(result).toEqual([]); }); it('has one valid study', async () => { const fileUrls = ['firstFileUrl']; const requiredTags = ['genetic']; + const studyWhitelist = []; const fetchFile = jest.fn(); fetchFile.mockReturnValueOnce(validStudy); - const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile }); + const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile }); expect(result).toEqual([validStudyOpenData]); }); it('has one valid study and one invalid study (Invalid ARN)', async () => { - const fileUrls = ['firstFileUrl']; + const fileUrls = ['firstFileUrl', 'fileUrl2']; const requiredTags = ['genetic']; + const studyWhitelist = []; const fetchFile = jest.fn(); fetchFile.mockReturnValueOnce(validStudy).mockReturnValueOnce(invalidStudy); - const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile }); + const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile }); expect(result).toEqual([validStudyOpenData]); }); + + it('has whitelisted study is included', async () => { + const fileUrls = ['firstFileUrl', 'fileUrl2', 'fileUrl3']; + const requiredTags = ['genetic']; + const studyWhitelist = ['study-3']; + const fetchFile = jest.fn(); + fetchFile + .mockReturnValueOnce(validStudy) + .mockReturnValueOnce(invalidStudy) + .mockReturnValueOnce(whitelistStudy); + + const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile }); + expect(result).toEqual([ + validStudyOpenData, + { + description: 'Example study 3', + id: 'study-3', + name: 'Study 3', + resources: [ + { + arn: 'arn:aws:s3:::study3', + description: 'Description for Study 3', + region: 'us-east-1', + type: 'S3 Bucket', + }, + ], + sha: 'abc2', + tags: ['other-tag'], + }, + ]); + }); }); diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js index a331699fea..3876abb240 100644 --- a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js +++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js @@ -42,7 +42,8 @@ if (typeof fetch !== 'function' && fetch.default && typeof fetch.default === 'fu fetch = fetch.default; } -const newHandler = async ({ studyService, log = consoleLogger } = {}) => { +const newHandler = async ({ studyService, log = consoleLogger, settings } = {}) => { + const whitelist = settings.get('openDataScrapeWhitelist') || ''; const scrape = { githubApiUrl: 'https://api.github.com', rawGithubUrl: 'https://raw.githubusercontent.com', @@ -50,6 +51,7 @@ const newHandler = async ({ studyService, log = consoleLogger } = {}) => { repository: 'open-data-registry', ref: 'main', subtree: 'datasets', + studyWhitelist: whitelist.split(',').filter(x => x), filterTags: [ 'genetic', 'genomic', @@ -193,15 +195,15 @@ const newHandler = async ({ studyService, log = consoleLogger } = {}) => { return async () => fetchAndSaveOpenData(fetchDatasetFiles, scrape, log, fetchFile, basicProjection, studyService); }; -const fetchOpenData = async ({ fileUrls, requiredTags, log, fetchFile }) => { +const fetchOpenData = async ({ fileUrls, requiredTags, studyWhitelist, log, fetchFile }) => { log.info(`Fetching ${fileUrls.length} metadata files`); const metadata = await Promise.all(fileUrls.map(fetchFile)); - log.info(`Filtering for ${requiredTags} tags and resources with valid ARNs`); + log.info(`Filtering for ${requiredTags} tags, ${studyWhitelist} whitelist studies, and resources with valid ARNs`); const validS3Arn = new RegExp(/^arn:aws:s3:.*:.*:.+$/); - const filtered = metadata.filter(({ tags, resources }) => { + const filtered = metadata.filter(({ id, tags, resources }) => { return ( - requiredTags.some(filterTag => tags.includes(filterTag)) && + (studyWhitelist.includes(id) || requiredTags.some(filterTag => tags.includes(filterTag))) && resources.every(resource => { return resource.type === 'S3 Bucket' && validS3Arn.test(resource.arn); }) @@ -237,7 +239,13 @@ async function saveOpenData(log, simplified, studyService) { const fetchAndSaveOpenData = async (fetchDatasetFiles, scrape, log, fetchFile, basicProjection, studyService) => { const fileUrls = await fetchDatasetFiles(); - const openData = await fetchOpenData({ fileUrls, requiredTags: scrape.filterTags, log, fetchFile }); + const openData = await fetchOpenData({ + fileUrls, + requiredTags: scrape.filterTags, + studyWhitelist: scrape.studyWhitelist, + log, + fetchFile, + }); const simplifiedStudyData = openData.map(basicProjection); diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js index b0ebb850f7..94e7ad18ec 100644 --- a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js +++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js @@ -26,7 +26,8 @@ const initHandler = (async () => { await container.initServices(); const studyService = await container.find('studyService'); const log = await container.find('log'); - return newHandler({ studyService, log }); + const settings = await container.find('settings'); + return newHandler({ studyService, log, settings }); })(); // eslint-disable-next-line import/prefer-default-export From 64741dcf2b2e5e2b600ead9591c53414af17c244 Mon Sep 17 00:00:00 2001 From: Samantha Date: Tue, 23 Jan 2024 11:19:56 -0500 Subject: [PATCH 3/5] [ALS-5158] Add Jira support widget (#161) * Append Jira support script at end of body element on app mount. --- main/solution/ui/public/index.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main/solution/ui/public/index.html b/main/solution/ui/public/index.html index 453c827498..fdd815a973 100644 --- a/main/solution/ui/public/index.html +++ b/main/solution/ui/public/index.html @@ -12,5 +12,10 @@ You need to enable JavaScript to run this app.
+ From cdad2319286ffb0528aa6de4f0b9a63b7f5a1093 Mon Sep 17 00:00:00 2001 From: Samantha Date: Wed, 24 Jan 2024 15:40:56 -0500 Subject: [PATCH 4/5] [ALS-5654] Add dropdown to help link if more than one url is given (#163) --- .../packages/main/src/parts/App.js | 36 ++++++++++++++++--- main/config/settings/example.yml | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/addons/addon-custom/packages/main/src/parts/App.js b/addons/addon-custom/packages/main/src/parts/App.js index 9ce9b2c849..2b8ca7f67c 100644 --- a/addons/addon-custom/packages/main/src/parts/App.js +++ b/addons/addon-custom/packages/main/src/parts/App.js @@ -18,7 +18,7 @@ import { Switch, Redirect, withRouter } from 'react-router-dom'; import { action, decorate, computed } from 'mobx'; import { inject, observer } from 'mobx-react'; import { getEnv } from 'mobx-state-tree'; -import { Menu } from 'semantic-ui-react'; +import { Menu, Dropdown, DropdownMenu, DropdownItem } from 'semantic-ui-react'; import { getRoutes, getMenuItems, getDefaultRouteLocation } from '@aws-ee/base-ui/dist/helpers/plugins-util'; import MainLayout from '@aws-ee/base-ui/dist/parts/MainLayout'; @@ -73,14 +73,42 @@ class RegisterApp extends React.Component { } } - openHelp() { - window.open(branding.page.help, '_blank'); + openHelp(url) { + return () => window.open(url, '_blank'); + } + + getLinks() { + const helpLinks = (branding.page.help || '').split(',').filter(x => x); + if (helpLinks.length < 2) { + return helpLinks; + } + return helpLinks.reduce((items, link) => { + const [text, ...urlParts] = link.split('='); + const url = urlParts.join('='); // if there are query params then we need to rebuild the url + if (!text || !url) { + console.error(`Poorly formed help link was provided: "${link}"`, 'Expected: "Text=URL"'); + return items; + } + return [...items, { text, url }]; + }, []); } appMenuItems() { + const links = this.getLinks(); return ( <> - {branding.page.help && Help} + {links.length === 1 && Help} + {links.length > 1 && ( + + + {links.map(({ text, url }) => ( + + {text} + + ))} + + + )} Terms of Service} closeOnDimmerClick diff --git a/main/config/settings/example.yml b/main/config/settings/example.yml index e4d85b2cfa..7b450c0962 100644 --- a/main/config/settings/example.yml +++ b/main/config/settings/example.yml @@ -163,5 +163,5 @@ HostedZoneId: 'Z02455261RJ9QQPVHFZGA' # Should the TOS link be enabled on the landing page. #tosLinkOnLanding: true -# URL for help link +# URL for help link. Can be empty, a single url, or a list like, "Some Link=https://www.google.com/?q=what,Other Link=https://www.example.com/" #helpUrl: about:blank \ No newline at end of file From 8d3edaf7a29221a6e89c5e2876813a83c3b74cfc Mon Sep 17 00:00:00 2001 From: Samantha Date: Thu, 25 Jan 2024 11:36:26 -0500 Subject: [PATCH 5/5] Update CHANGELOG.md (#164) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aacaae06d4..14d2c0fed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. As our fork has diverged from AWS SWB mainline branch, we are noting the SWB version and the lab version together, as \_, starting from SWB mainline, 5.0.0. +## [5.0.0_1.4.0](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.3.2...v5.0.0_1.4.0) (01/25/2024) +* Parameterize user register TOS acceptance. +* Add comma separated study whitelist in stage file. +* Add Jira support widget. +* Add dropdown to help link if more than one url is given. + ## [5.0.0_1.3.2](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.3.1...v5.0.0_1.3.2) (12/21/2023) - Update register page email regex validation: validatorjs schema regex fields need slashes before and after the regex, or else it returns a validation error.