diff --git a/.gitallowed b/.gitallowed index 7e1c822f0e..191429d229 100644 --- a/.gitallowed +++ b/.gitallowed @@ -1,2 +1,3 @@ core/src/test/scala/org/broadinstitute/dsde/rawls/model/ExecutionModelSpec.scala core/src/test/scala/org/broadinstitute/dsde/rawls/fastpass/MockFastPassService.scala +automation/setup-local-env-for-e2e.sh diff --git a/.github/workflows/rawls-run-azure-e2e-tests.yaml b/.github/workflows/rawls-run-azure-e2e-tests.yaml index 45b5229941..23d52246fb 100644 --- a/.github/workflows/rawls-run-azure-e2e-tests.yaml +++ b/.github/workflows/rawls-run-azure-e2e-tests.yaml @@ -1,5 +1,17 @@ name: rawls-run-azure-e2e-tests +# This workflow can be run on schedule OR dispatched (manually or by another dispatcher workflow) +# When thw workflow is triggered on schedule, it will use the parameters specified on +# the RHS of || in init-github-context.extract-inputs step. Otherwise, if the workflow is +# triggered by a dispatch event, it will use the parameters specified on LHS of || in +# the init-github-context.extract-inputs step. +# +# Ex. +# +# echo "student-subjects=${{ toJson(inputs.student-subjects || '["harry.potter@quality.firecloud.org","lavender.brown@quality.firecloud.org"]') }}" >> "$GITHUB_OUTPUT" +# +# On schedule: student-subjects output will use '["harry.potter@quality.firecloud.org","lavender.brown@quality.firecloud.org"]' +# Dispatch event: student-subjects output will use inputs.student-subjects on: schedule: # run twice a day at 10:00 and 22:00 UTC every day of the week @@ -16,10 +28,28 @@ on: required: true default: true type: boolean + owner-subject: + description: 'Owner subject (used for creating billing project in E2E testing)' + required: true + default: 'hermione.owner@quality.firecloud.org' + type: string + student-subjects: + description: 'A JSON array of Student subjects used for E2E testing' + required: true + default: '["harry.potter@quality.firecloud.org","ron.weasley@quality.firecloud.org"]' + type: string + service-account: + description: 'Email address or unique identifier of the Google Cloud service account for which to generate credentials' + required: true + default: 'firecloud-qa@broad-dsde-qa.iam.gserviceaccount.com' + type: string +# E2E_ENV is the name of a .env file that contains envvars for E2E tests env: - BEE_NAME: '${{ github.event.repository.name }}-${{ github.run_id }}-${{ github.run_attempt}}-dev' + RUN_NAME_SUFFIX: '${{ github.event.repository.name }}-${{ github.run_id }}-${{ github.run_attempt }}' + BEE_NAME: '${{ github.event.repository.name }}-${{ github.run_id }}-${{ github.run_attempt }}-dev' TOKEN: '${{ secrets.BROADBOT_TOKEN }}' # github token for access to kick off a job in the private repo + E2E_ENV: 'azure_e2e.env' jobs: init-github-context: @@ -27,12 +57,18 @@ jobs: outputs: branch: ${{ steps.extract-inputs.outputs.branch }} delete-bee: ${{ steps.extract-inputs.outputs.delete-bee }} + student-subjects: ${{ steps.extract-inputs.outputs.student-subjects }} + owner-subject: ${{ steps.extract-inputs.outputs.owner-subject }} + service-account: ${{ steps.extract-inputs.outputs.service-account }} steps: - name: Get inputs or use defaults id: extract-inputs run: | - echo "branch=${{ inputs.branch || 'develop' }}" >> "$GITHUB_OUTPUT" - echo "delete-bee=${{ inputs.delete-bee || false }}" >> "$GITHUB_OUTPUT" + echo "branch=${{ inputs.branch || 'develop' }}" >> "$GITHUB_OUTPUT" + echo "delete-bee=${{ inputs.delete-bee || false }}" >> "$GITHUB_OUTPUT" + echo "owner-subject=${{ inputs.owner-subject || 'hermione.owner@quality.firecloud.org' }}" >> "$GITHUB_OUTPUT" + echo "student-subjects=${{ toJson(inputs.student-subjects || '["harry.potter@quality.firecloud.org","ron.weasley@quality.firecloud.org"]') }}" >> "$GITHUB_OUTPUT" + echo "service-account=${{ inputs.service-account || 'firecloud-qa@broad-dsde-qa.iam.gserviceaccount.com' }}" >> "$GITHUB_OUTPUT" rawls-build-tag-publish-job: runs-on: ubuntu-latest @@ -63,7 +99,11 @@ jobs: repo: broadinstitute/terra-github-workflows ref: refs/heads/main token: ${{ env.TOKEN }} - inputs: '{ "repository": "${{ github.event.repository.full_name }}", "ref": "refs/heads/${{ needs.init-github-context.outputs.branch }}", "rawls-release-tag": "${{ steps.tag.outputs.tag }}" }' + inputs: '{ + "repository": "${{ github.event.repository.full_name }}", + "ref": "refs/heads/${{ needs.init-github-context.outputs.branch }}", + "rawls-release-tag": "${{ steps.tag.outputs.tag }}" + }' - name: Render Rawls version id: render-rawls-version @@ -91,15 +131,57 @@ jobs: repo: broadinstitute/terra-github-workflows ref: refs/heads/main token: ${{ env.TOKEN }} - inputs: '{ "bee-name": "${{ env.BEE_NAME }}", "bee-template-name": "rawls-e2e-azure-tests", "version-template": "dev", "custom-version-json": "${{ needs.rawls-build-tag-publish-job.outputs.custom-version-json }}" }' + inputs: '{ + "bee-name": "${{ env.BEE_NAME }}", + "bee-template-name": "rawls-e2e-azure-tests", + "version-template": "dev", + "custom-version-json": "${{ needs.rawls-build-tag-publish-job.outputs.custom-version-json }}" + }' + + # This job can be used for generating parameters for E2E tests (e.g. a random project name). + params-gen: + runs-on: ubuntu-latest + outputs: + project-name: ${{ steps.gen.outputs.project_name }} + steps: + - uses: 'actions/checkout@v3' + + - name: Generate a random billing project name + id: 'gen' + run: | + project_name=$(echo "tmp-billing-project-$(uuidgen)" | cut -c -30) + echo "project_name=${project_name}" >> $GITHUB_OUTPUT + + # Azure Managed App Coordinates are defined in the following workflow: + # https://github.com/broadinstitute/terra-github-workflows/blob/main/.github/workflows/attach-landing-zone-to-bee.yaml + attach-billing-project-to-landing-zone-workflow: + runs-on: ubuntu-latest + needs: [init-github-context, create-bee-workflow, params-gen] + steps: + - name: dispatch to terra-github-workflows + uses: broadinstitute/workflow-dispatch@v3 + with: + workflow: attach-billing-project-to-landing-zone.yaml + repo: broadinstitute/terra-github-workflows + ref: refs/heads/main + token: ${{ env.TOKEN }} + inputs: '{ + "run-name": "attach-billing-project-to-landing-zone-${{ env.RUN_NAME_SUFFIX }}", + "bee-name": "${{ env.BEE_NAME }}", + "billing-project": "${{ needs.params-gen.outputs.project-name }}", + "billing-project-creator": "${{ needs.init-github-context.outputs.owner-subject }}", + "service-account": "${{ needs.init-github-context.outputs.service-account }}" + }' rawls-swat-e2e-test-job: runs-on: ubuntu-latest - needs: [create-bee-workflow, init-github-context] - permissions: - contents: 'read' - id-token: 'write' + needs: [init-github-context, create-bee-workflow, params-gen, attach-billing-project-to-landing-zone-workflow] steps: + - name: Configure the user subjects for the test + run: | + escapedJSON=$(echo '${{ needs.init-github-context.outputs.student-subjects }}' | sed 's/"/\"/g') + echo "USER_SUBJECTS={\"service_account\":\"${{ needs.init-github-context.outputs.service-account }}\", \"owners\": [\"${{ needs.init-github-context.outputs.owner-subject }}\"], \"students\": $escapedJSON}" >> $GITHUB_ENV + - name: dispatch to terra-github-workflows env: rawls_test_command: "testOnly -- -l ProdTest -l NotebooksCanaryTest -n org.broadinstitute.dsde.test.api.WorkspacesAzureTest" @@ -109,15 +191,44 @@ jobs: repo: broadinstitute/terra-github-workflows ref: refs/heads/main token: ${{ env.TOKEN }} - inputs: '{ "bee-name": "${{ env.BEE_NAME }}", "ENV": "qa", "ref": "refs/heads/${{ needs.init-github-context.outputs.branch }}", "test-group-name": "workspaces_azure", "test-command": "${{ env.rawls_test_command }}", "java-version": "17" }' + inputs: '{ + "run-name": "rawls-swat-tests-${{ env.RUN_NAME_SUFFIX }}", + "bee-name": "${{ env.BEE_NAME }}", + "ENV": "qa", + "ref": "refs/heads/${{ needs.init-github-context.outputs.branch }}", + "test-group-name": "workspaces_azure", + "test-command": "${{ env.rawls_test_command }}", + "java-version": "17", + "billing-project": "${{ needs.params-gen.outputs.project-name }}", + "e2e-env": "${{ env.E2E_ENV }}", + "user-subjects": ${{ toJson(env.USER_SUBJECTS) }} + }' + + delete-billing-project-v2-from-bee-workflow: + runs-on: ubuntu-latest + needs: [init-github-context, params-gen, rawls-swat-e2e-test-job] + if: always() + steps: + - name: dispatch to terra-github-workflows + uses: broadinstitute/workflow-dispatch@v3 + with: + workflow: .github/workflows/delete-billing-project-v2-from-bee.yaml + repo: broadinstitute/terra-github-workflows + ref: refs/heads/main + token: ${{ env.TOKEN }} + inputs: '{ + "run-name": "delete-billing-project-v2-from-bee-${{ env.RUN_NAME_SUFFIX }}", + "bee-name": "${{ env.BEE_NAME }}", + "billing-project": "${{ needs.params-gen.outputs.project-name }}", + "billing-project-owner": "${{ needs.init-github-context.outputs.owner-subject }}", + "service-account": "${{ needs.init-github-context.outputs.service-account }}", + "silent-on-failure": "false" + }' destroy-bee-workflow: runs-on: ubuntu-latest - needs: [rawls-swat-e2e-test-job, init-github-context] + needs: [init-github-context, rawls-swat-e2e-test-job, delete-billing-project-v2-from-bee-workflow] if: ${{ needs.init-github-context.outputs.delete-bee && always() }} # always run to confirm bee is destroyed unless explicitly requested not to - permissions: - contents: 'read' - id-token: 'write' steps: - name: dispatch to terra-github-workflows uses: broadinstitute/workflow-dispatch@v3 @@ -127,6 +238,7 @@ jobs: ref: refs/heads/main token: ${{ env.TOKEN }} inputs: '{ "bee-name": "${{ env.BEE_NAME }}" }' + wait-for-completion: false notify-slack-on-failure: runs-on: ubuntu-latest diff --git a/automation/application.bee.conf.template b/automation/application.bee.conf.template new file mode 100644 index 0000000000..a04b24dfab --- /dev/null +++ b/automation/application.bee.conf.template @@ -0,0 +1,62 @@ +# To run against a local UI, set baseUrl = "http://local.broadinstitute.org/" +fireCloud { + baseUrl = "https://firecloudui.${BEE_ENV}.bee.envs-terra.bio/" + + + orchApiUrl = "https://firecloudorch.${BEE_ENV}.bee.envs-terra.bio/" + rawlsApiUrl = "https://rawls.${BEE_ENV}.bee.envs-terra.bio/" + samApiUrl = "https://sam.${BEE_ENV}.bee.envs-terra.bio/" + thurloeApiUrl = "https://thurloe.${BEE_ENV}.bee.envs-terra.bio/" + workspaceManagerApiUrl = "https://workspace.${BEE_ENV}.bee.envs-terra.bio/" + + + fireCloudId = "${FC_ID}" + tcgaAuthDomain = "TCGA-dbGaP-Authorized" + + + # fiab integration tests should use TDR alpha environment + dataRepoApiUrl = "https://data.alpha.bee.envs-terra.bio/" + + + + gpAllocApiUrl = "https://gpalloc-qa.dsp-techops.broadinstitute.org/api/" + + waitForAccessDuration=3m +} + +gcs { + appsDomain = "quality.firecloud.org" + qaEmail = "${QA_EMAIL}" + serviceProject = "broad-dsde-qa" + smoketestsProject = "broad-dsde-qa" + qaPemFile = "${SCRIPT_DIR}/src/test/resources/firecloud-account.pem" + qaJsonFile = "${SCRIPT_DIR}/src/test/resources/firecloud-account.json" + trialBillingPemFile = "${SCRIPT_DIR}/src/test/resources/trial-billing-account.pem" + trialBillingPemFileClientId = "${TRIAL_BILLING_CLIENT_ID}" + orchStorageSigningSA = "${ORCH_STORAGE_SIGNING_SA}" + billingAccount = "Broad Institute - 8201528" + billingAccountId = "${BILLING_ACCOUNT_ID}" + subEmail = "google@quality.firecloud.org" + googleAccessPolicy = "891321614892" +} + +users { + notSoSecretPassword = "${AUTO_USERS_PASSWD}" + + userDataPath = "${SCRIPT_DIR}/src/test/resources/users.json" + # for smoketests + smoketestpassword = "${USERS_PASSWD}" + smoketestuser = "hermione.owner@quality.firecloud.org" +} + +methods { + testMethod = "DO_NOT_CHANGE_test_method" + testMethodConfig = "DO_NOT_CHANGE_test1_config" + methodConfigNamespace = "automationmethods" + snapshotID = 1 +} + +chromeSettings { + chromedriverHost = "http://hub:4444/wd/hub" + chromedriverPath = "/usr/local/bin/chromedriver" +} diff --git a/automation/project/Dependencies.scala b/automation/project/Dependencies.scala index a6cdf580f9..2c051a7183 100644 --- a/automation/project/Dependencies.scala +++ b/automation/project/Dependencies.scala @@ -55,6 +55,7 @@ object Dependencies { "com.typesafe.akka" %% "akka-slf4j" % akkaV, "org.specs2" %% "specs2-core" % "4.15.0" % Test, "org.scalatest" %% "scalatest" % "3.2.2" % Test, + "io.circe" %% "circe-generic" % "0.14.2" % Test, "org.seleniumhq.selenium" % "selenium-java" % "3.8.1" % Test, "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", "org.broadinstitute.dsde" %% "rawls-model" % "0.1-3891e8393" diff --git a/automation/setup-local-env-for-e2e.sh b/automation/setup-local-env-for-e2e.sh new file mode 100755 index 0000000000..67da16ea2e --- /dev/null +++ b/automation/setup-local-env-for-e2e.sh @@ -0,0 +1,259 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_DIR=$(pwd) +TEST_RESOURCES=${SCRIPT_DIR}/src/test/resources + +# Required values +e2eEnv="" +bee="" +billingProject="" + +# Function to display usage/help message +usage() { + echo "Usage: $0 --e2eEnv --bee --billingProject " + echo " --e2eEnv: The name of the .env file that contains envvars for E2E tests (e.g. The Base64-encoded User Data in JSON format (ex: [{\"email\":\"hermione.owner@quality.firecloud.org\",\"type\":\"owner\",\"bearer\":\"yadayada\"},{\"email\":\"harry.potter@quality.firecloud.org\",\"type\":\"student\",\"bearer\":\"yadayada2\"}]) is stored in USERS_METADATA_B64 envvar." + echo " --bee: The name of an existing BEE environment." + echo " --billingProject: The name of a valid billing project in the given BEE environment that has already been attached to Azure Landing Zone.." + echo "" + echo "# Use Case" + echo "# =========" + echo "#" + echo "# $0 --e2eEnv azure_e2e.env --bee rawls-593017852-1-dev --billingProject tmp-billing-project-44302a2c-5" + echo "#" + echo "# Replace rawls-593017852-1-dev with the name of an existing BEE environment you want your tests to run against" + echo "# Replace tmp-billing-project-44302a2c-5 with the name of a valid billing project in the BEE environment" + echo "# Please refer to https://github.com/broadinstitute/terra-github-workflows/blob/main/.github/workflows/attach-billing-project-to-landing-zone.yaml" + echo "#" + echo "# The above script generates a azure_e2e.env file (or whatever filename you configured for the --e2eEnv argument) under the src/test/resources directory. You can run tests locally as follows." + echo "#" + echo "# source ${TEST_RESOURCES}/azure_e2e.env" + echo "# sbt \"testOnly -- -l ProdTest -l NotebooksCanaryTest -n org.broadinstitute.dsde.test.api.WorkspacesAzureTest\"" + echo "#" + echo "# If you are running the test via IntelliJ, in the Run/Debug Configuration use the EnvFile tab to provide the path to the generated .env file." + echo "#" + exit 1 +} + +# Parse command-line arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --e2eEnv) + e2eEnv="$2" + shift 2 + ;; + --bee) + bee="$2" + shift 2 + ;; + --billingProject) + billingProject="$2" + shift 2 + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# Check if required arguments are provided +if [[ -z "$e2eEnv" || -z "$bee" || -z "$billingProject" ]]; then + echo "Usage: $0 --e2eEnv --bee --billingProject " + echo "Use '$0 --help' to see all available options." + exit 1 +fi + +echo "export SCRIPT_DIR=\"${SCRIPT_DIR}\"" > ${TEST_RESOURCES}/$e2eEnv +echo "export E2E_ENV=${e2eEnv}" >> ${TEST_RESOURCES}/$e2eEnv +echo "export BEE_ENV=\"${bee}\"" >> ${TEST_RESOURCES}/$e2eEnv +echo "export BILLING_PROJECT=\"${billingProject}\"" >> ${TEST_RESOURCES}/$e2eEnv + +VAULT_TOKEN=$(cat $HOME/.vault-token) +DSDE_TOOLBOX_DOCKER_IMAGE=broadinstitute/dsde-toolbox:latest +FC_ACCOUNT_PATH=secret/dsde/firecloud/qa/common/firecloud-account.json +TRIAL_BILLING_ACCOUNT_PATH=secret/dsde/firecloud/qa/common/trial-billing-account.json +FC_SECRETS_PATH=secret/dsde/firecloud/qa/common/secrets +FC_USERS_PATH=secret/dsde/firecloud/qa/common/users +RAWLS_ACCOUNT_PATH=secret/dsde/firecloud/qa/rawls/rawls-account.json + +docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_ACCOUNT_PATH} \ + | jq -r .data > ${TEST_RESOURCES}/firecloud-account.json + +docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_ACCOUNT_PATH} \ + | jq -r .data.private_key > ${TEST_RESOURCES}/firecloud-account.pem + +docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${TRIAL_BILLING_ACCOUNT_PATH} \ + | jq -r .data.private_key > ${TEST_RESOURCES}/trial-billing-account.pem + +cat << EOF > ${TEST_RESOURCES}/users.json +{ + "admins": { + "dumbledore": "dumbledore.admin@quality.firecloud.org", + "voldemort": "voldemort.admin@quality.firecloud.org" + }, + "owners": { + "hermione": "hermione.owner@quality.firecloud.org", + "sirius": "sirius.owner@quality.firecloud.org", + "tonks": "tonks.owner@quality.firecloud.org" + }, + "curators": { + "mcgonagall": "mcgonagall.curator@quality.firecloud.org", + "snape": "snape.curator@quality.firecloud.org", + "hagrid": "hagrid.curator@quality.firecloud.org", + "lupin": "lupin.curator@quality.firecloud.org", + "flitwick": "flitwick.curator@quality.firecloud.org" + }, + "authdomains": { + "fred": "fred.authdomain@quality.firecloud.org", + "george": "george.authdomain@quality.firecloud.org", + "bill": "bill.authdomain@quality.firecloud.org", + "percy": "percy.authdomain@quality.firecloud.org", + "molly": "molly.authdomain@quality.firecloud.org", + "arthur": "arthur.authdomain@quality.firecloud.org" + }, + "students": { + "harry": "harry.potter@quality.firecloud.org", + "ron": "ron.weasley@quality.firecloud.org", + "lavender": "lavender.brown@quality.firecloud.org", + "cho": "cho.chang@quality.firecloud.org", + "oliver": "oliver.wood@quality.firecloud.org", + "cedric": "cedric.diggory@quality.firecloud.org", + "crabbe": "vincent.crabbe@quality.firecloud.org", + "goyle": "gregory.goyle@quality.firecloud.org", + "dean": "dean.thomas@quality.firecloud.org", + "ginny": "ginny.weasley@quality.firecloud.org" + }, + "temps": { + "luna": "luna.temp@quality.firecloud.org", + "neville": "neville.temp@quality.firecloud.org" + }, + "notebookswhitelisted": { + "hermione": "hermione.owner@quality.firecloud.org", + "ron": "ron.weasley@quality.firecloud.org" + }, + "campaignManagers": { + "dumbledore": "dumbledore.admin@quality.firecloud.org", + "voldemort": "voldemort.admin@quality.firecloud.org" + } +} +EOF + +FC_ID=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_SECRETS_PATH} \ + | jq -r .data.firecloud_id) + +echo "export FC_ID=${FC_ID}" >> ${TEST_RESOURCES}/$e2eEnv + +QA_EMAIL=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_USERS_PATH} \ + | jq -r .data.service_acct_email) + +echo "export QA_EMAIL=${QA_EMAIL}" >> ${TEST_RESOURCES}/$e2eEnv + +TRIAL_BILLING_CLIENT_ID=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${TRIAL_BILLING_ACCOUNT_PATH} \ + | jq -r .data.client_email) + +echo "export TRIAL_BILLING_CLIENT_ID=${TRIAL_BILLING_CLIENT_ID}" >> ${TEST_RESOURCES}/$e2eEnv + +ORCH_STORAGE_SIGNING_SA=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${RAWLS_ACCOUNT_PATH} \ + | jq -r .data.client_email) + +echo "export ORCH_STORAGE_SIGNING_SA=${ORCH_STORAGE_SIGNING_SA}" >> ${TEST_RESOURCES}/$e2eEnv + +BILLING_ACCOUNT_ID=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_SECRETS_PATH} \ + | jq -r .data.trial_billing_account) + +echo "export BILLING_ACCOUNT_ID=${BILLING_ACCOUNT_ID}" >> ${TEST_RESOURCES}/$e2eEnv + +AUTO_USERS_PASSWD=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_USERS_PATH} \ + | jq -r .data.automation_users_passwd) + +echo "export AUTO_USERS_PASSWD=${AUTO_USERS_PASSWD}" >> ${TEST_RESOURCES}/$e2eEnv + +USERS_PASSWD=$(docker run --rm -e VAULT_TOKEN=$VAULT_TOKEN \ + ${DSDE_TOOLBOX_DOCKER_IMAGE} \ + vault read --format=json ${FC_USERS_PATH} \ + | jq -r .data.users_passwd) + +echo "export USERS_PASSWD=${USERS_PASSWD}" >> ${TEST_RESOURCES}/$e2eEnv + +# Function to obtain user tokens +obtainUserTokens() { + echo "Obtaining user tokens for hermione, harry, and ron..." + hermione=$(docker run --rm -v ${SCRIPT_DIR}/src:/src \ + -i gcr.io/oauth2l/oauth2l fetch \ + --credentials /src/test/resources/firecloud-account.json \ + --scope profile,email,openid \ + --email hermione.owner@quality.firecloud.org) + + harry=$(docker run --rm -v ${SCRIPT_DIR}/src:/src \ + -i gcr.io/oauth2l/oauth2l fetch \ + --credentials /src/test/resources/firecloud-account.json \ + --scope profile,email,openid \ + --email harry.potter@quality.firecloud.org) + + ron=$(docker run --rm -v ${SCRIPT_DIR}/src:/src \ + -i gcr.io/oauth2l/oauth2l fetch \ + --credentials /src/test/resources/firecloud-account.json \ + --scope profile,email,openid \ + --email ron.weasley@quality.firecloud.org) + + USERS_METADATA_JSON="[ + { + \"email\":\"hermione.owner@quality.firecloud.org\", + \"type\":\"owner\", + \"bearer\":\"${hermione}\" + }, + { + \"email\":\"harry.potter@quality.firecloud.org\", + \"type\":\"student\", + \"bearer\":\"${harry}\" + }, + { + \"email\":\"ron.weasly@quality.firecloud.org\", + \"type\":\"student\", + \"bearer\":\"${ron}\" + } + ]" + + # Try the -w0 option (without line breaks) first + if base64 --help | grep -q '\-w'; then + USERS_METADATA_JSON_B64=$(printf '%s' "${USERS_METADATA_JSON}" | base64 -w0) + # If -w0 is not available, try the -b0 option (also without line breaks) + elif base64 --help | grep -q '\-b'; then + USERS_METADATA_JSON_B64=$(printf '%s' "${USERS_METADATA_JSON}" | base64 -b 0) + else + echo "Error: No suitable base64 encoding option found." + exit 1 + fi + + echo "export USERS_METADATA_B64=\"${USERS_METADATA_JSON_B64}\"" >> ${TEST_RESOURCES}/$e2eEnv +} + +obtainUserTokens + +source ${TEST_RESOURCES}/$e2eEnv + +# Read the template file and perform the substitution +template="application.bee.conf.template" +conf="${TEST_RESOURCES}/application.conf" + +envsubst < ${template} > ${conf} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/test/api/WorkspacesAzureApiSpec.scala b/automation/src/test/scala/org/broadinstitute/dsde/test/api/WorkspacesAzureApiSpec.scala index 3e3e8e8ac1..fc18899d65 100644 --- a/automation/src/test/scala/org/broadinstitute/dsde/test/api/WorkspacesAzureApiSpec.scala +++ b/automation/src/test/scala/org/broadinstitute/dsde/test/api/WorkspacesAzureApiSpec.scala @@ -10,17 +10,15 @@ import akka.util.ByteString import org.broadinstitute.dsde.rawls.model.WorkspaceAccessLevels.ProjectOwner import org.broadinstitute.dsde.rawls.model.WorkspaceJsonSupport._ import org.broadinstitute.dsde.rawls.model.{ - AzureManagedAppCoordinates, WorkspaceCloudPlatform, WorkspaceListResponse, WorkspaceResponse, WorkspaceType } import org.broadinstitute.dsde.workbench.auth.AuthToken -import org.broadinstitute.dsde.workbench.config.{Credentials, UserPool} -import org.broadinstitute.dsde.workbench.fixture.BillingFixtures.withTemporaryAzureBillingProject import org.broadinstitute.dsde.workbench.service.test.CleanUp import org.broadinstitute.dsde.workbench.service.{Orchestration, Rawls, RestException, WorkspaceAccessLevel} +import org.scalatest.BeforeAndAfterAll import org.scalatest.concurrent.Eventually.eventually import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -31,48 +29,58 @@ import spray.json._ import java.util.UUID import scala.concurrent.{Await, Future} import scala.language.postfixOps +import org.broadinstitute.dsde.test.pipeline._ @WorkspacesAzureTest -class AzureWorkspacesSpec extends AnyFlatSpec with Matchers with CleanUp { - val owner: Credentials = UserPool.userConfig.Owners.getUserCredential("hermione") - val nonOwner: Credentials = UserPool.chooseStudent - - private val azureManagedAppCoordinates = AzureManagedAppCoordinates( - UUID.fromString("fad90753-2022-4456-9b0a-c7e5b934e408"), - UUID.fromString("f557c728-871d-408c-a28b-eb6b2141a087"), - "staticTestingMrg", - Some(UUID.fromString("f41c1a97-179b-4a18-9615-5214d79ba600")) - ) +class WorkspacesAzureApiSpec extends AnyFlatSpec with Matchers with BeforeAndAfterAll with CleanUp { + // The values of the following vars are injected from the pipeline. + var billingProject: String = _ + var ownerAuthToken: ProxyAuthToken = _ + var nonOwnerAuthToken: ProxyAuthToken = _ private val wsmUrl = RawlsConfig.wsmUrl implicit val system = ActorSystem() + override def beforeAll(): Unit = { + val bee = PipelineInjector(PipelineInjector.e2eEnv()) + billingProject = bee.billingProject + bee.Owners.getUserCredential("hermione") match { + case Some(owner) => + ownerAuthToken = owner.makeAuthToken + case _ => () + } + bee.chooseStudent match { + case Some(student) => + nonOwnerAuthToken = student.makeAuthToken + case _ => () + } + } + "Rawls" should "allow creation and deletion of azure workspaces" in { - implicit val token = owner.makeAuthToken() - withTemporaryAzureBillingProject(azureManagedAppCoordinates) { projectName => - val workspaceName = generateWorkspaceName() - Rawls.workspaces.create( - projectName, - workspaceName, - Set.empty, - Map("disableAutomaticAppCreation" -> "true") - ) - try { - val response = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceName)) - response.workspace.name should be(workspaceName) - response.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) - response.workspace.workspaceType should be(Some(WorkspaceType.McWorkspace)) - response.accessLevel should be(Some(ProjectOwner)) - } finally { - Rawls.workspaces.delete(projectName, workspaceName) - assertNoAccessToWorkspace(projectName, workspaceName) - } + implicit val token = ownerAuthToken + val projectName = billingProject + val workspaceName = generateWorkspaceName() + Rawls.workspaces.create( + projectName, + workspaceName, + Set.empty, + Map("disableAutomaticAppCreation" -> "true") + ) + try { + val response = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceName)) + response.workspace.name should be(workspaceName) + response.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) + response.workspace.workspaceType should be(Some(WorkspaceType.McWorkspace)) + response.accessLevel should be(Some(ProjectOwner)) + } finally { + Rawls.workspaces.delete(projectName, workspaceName) + assertNoAccessToWorkspace(projectName, workspaceName) } } it should "allow access to WorkspaceManager API" in { - implicit val token = owner.makeAuthToken() + implicit val token = ownerAuthToken val statusRequest = Rawls.getRequest(wsmUrl + "status") withClue(s"WSM status API returned ${statusRequest.status.intValue()} ${statusRequest.status.reason()}!") { @@ -81,171 +89,167 @@ class AzureWorkspacesSpec extends AnyFlatSpec with Matchers with CleanUp { } it should "allow cloning of azure workspaces" in { - implicit val token = owner.makeAuthToken() - withTemporaryAzureBillingProject(azureManagedAppCoordinates) { projectName => - val workspaceName = generateWorkspaceName() - val workspaceCloneName = generateWorkspaceName() - - val analysesDir = "analyses" - val analysesFilename = analysesDir + "/testFile.txt" - val analysesContents = "hello world" + implicit val token = ownerAuthToken + val projectName = billingProject + val workspaceName = generateWorkspaceName() + val workspaceCloneName = generateWorkspaceName() + + val analysesDir = "analyses" + val analysesFilename = analysesDir + "/testFile.txt" + val analysesContents = "hello world" + + val nonAnalysesFilename = "willNotClone.txt" + val nonAnalysesContents = "user upload content" + + Rawls.workspaces.create( + projectName, + workspaceName, + Set.empty, + Map("disableAutomaticAppCreation" -> "true") + ) + try { + val sasUrl = getSasUrl(projectName, workspaceName, token) + + // Upload the blob that will be cloned + uploadBlob(sasUrl, analysesFilename, analysesContents) + val downloadContents = downloadBlob(sasUrl, analysesFilename) + withClue(s"testing uploaded blob ${analysesFilename}") { + downloadContents shouldBe analysesContents + } - val nonAnalysesFilename = "willNotClone.txt" - val nonAnalysesContents = "user upload content" + // Upload the blob that should not be cloned + uploadBlob(sasUrl, nonAnalysesFilename, nonAnalysesContents) + val downloadNonAnalysesContents = downloadBlob(sasUrl, nonAnalysesFilename) + withClue(s"testing uploaded blob ${nonAnalysesFilename}") { + downloadNonAnalysesContents shouldBe nonAnalysesContents + } - Rawls.workspaces.create( + Rawls.workspaces.clone( projectName, workspaceName, + projectName, + workspaceCloneName, Set.empty, + Some(analysesDir), Map("disableAutomaticAppCreation" -> "true") ) try { - val sasUrl = getSasUrl(projectName, workspaceName, token) - - // Upload the blob that will be cloned - uploadBlob(sasUrl, analysesFilename, analysesContents) - val downloadContents = downloadBlob(sasUrl, analysesFilename) - withClue(s"testing uploaded blob ${analysesFilename}") { - downloadContents shouldBe analysesContents + val clonedResponse = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceCloneName)) + clonedResponse.workspace.name should equal(workspaceCloneName) + clonedResponse.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) + clonedResponse.workspace.workspaceType should be(Some(WorkspaceType.McWorkspace)) + clonedResponse.accessLevel should be(Some(ProjectOwner)) + + withClue(s"Verifying container cloning has completed") { + awaitCond( + isCloneCompleted(projectName, workspaceCloneName), + 60 seconds, + 2 seconds + ) } - // Upload the blob that should not be cloned - uploadBlob(sasUrl, nonAnalysesFilename, nonAnalysesContents) - val downloadNonAnalysesContents = downloadBlob(sasUrl, nonAnalysesFilename) - withClue(s"testing uploaded blob ${nonAnalysesFilename}") { - downloadNonAnalysesContents shouldBe nonAnalysesContents + val cloneSasUrl = getSasUrl(projectName, workspaceCloneName, token) + val downloadCloneContents = downloadBlob(cloneSasUrl, analysesFilename) + withClue(s"testing blob ${analysesFilename} cloned") { + downloadCloneContents shouldBe analysesContents } - - Rawls.workspaces.clone( - projectName, - workspaceName, - projectName, - workspaceCloneName, - Set.empty, - Some(analysesDir), - Map("disableAutomaticAppCreation" -> "true") - ) - try { - val clonedResponse = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceCloneName)) - clonedResponse.workspace.name should equal(workspaceCloneName) - clonedResponse.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) - clonedResponse.workspace.workspaceType should be(Some(WorkspaceType.McWorkspace)) - clonedResponse.accessLevel should be(Some(ProjectOwner)) - - withClue(s"Verifying container cloning has completed") { - awaitCond( - isCloneCompleted(projectName, workspaceCloneName), - 60 seconds, - 2 seconds - ) - } - - val cloneSasUrl = getSasUrl(projectName, workspaceCloneName, token) - val downloadCloneContents = downloadBlob(cloneSasUrl, analysesFilename) - withClue(s"testing blob ${analysesFilename} cloned") { - downloadCloneContents shouldBe analysesContents - } - withClue(s"testing blob ${nonAnalysesFilename} did not clone") { - verifyBlobNotCloned(cloneSasUrl, nonAnalysesFilename) - } - } finally { - Rawls.workspaces.delete(projectName, workspaceCloneName) - assertNoAccessToWorkspace(projectName, workspaceCloneName) + withClue(s"testing blob ${nonAnalysesFilename} did not clone") { + verifyBlobNotCloned(cloneSasUrl, nonAnalysesFilename) } } finally { - Rawls.workspaces.delete(projectName, workspaceName) - assertNoAccessToWorkspace(projectName, workspaceName) + Rawls.workspaces.delete(projectName, workspaceCloneName) + assertNoAccessToWorkspace(projectName, workspaceCloneName) } + } finally { + Rawls.workspaces.delete(projectName, workspaceName) + assertNoAccessToWorkspace(projectName, workspaceName) } } it should "allow listing workspaces" in { - implicit val token = owner.makeAuthToken() - withTemporaryAzureBillingProject(azureManagedAppCoordinates) { projectName => - val workspaceName1 = generateWorkspaceName() - val workspaceName2 = generateWorkspaceName() + implicit val token = ownerAuthToken + val projectName = billingProject + val workspaceName1 = generateWorkspaceName() + val workspaceName2 = generateWorkspaceName() - try { - Rawls.workspaces.create( - projectName, - workspaceName1, - Set.empty, - Map("disableAutomaticAppCreation" -> "true") - ) - Rawls.workspaces.create( - projectName, - workspaceName2, - Set.empty, - Map("disableAutomaticAppCreation" -> "true") - ) - - val workspaces = Rawls.workspaces.list().parseJson.convertTo[Seq[WorkspaceListResponse]] - - workspaces.length shouldBe 2 - workspaces.map(_.workspace.name).toSet shouldBe Set(workspaceName1, workspaceName2) - } finally { - Rawls.workspaces.delete(projectName, workspaceName1) - assertNoAccessToWorkspace(projectName, workspaceName1) + try { + Rawls.workspaces.create( + projectName, + workspaceName1, + Set.empty, + Map("disableAutomaticAppCreation" -> "true") + ) + Rawls.workspaces.create( + projectName, + workspaceName2, + Set.empty, + Map("disableAutomaticAppCreation" -> "true") + ) - Rawls.workspaces.delete(projectName, workspaceName2) - assertNoAccessToWorkspace(projectName, workspaceName2) - } + val workspaces = Rawls.workspaces.list().parseJson.convertTo[Seq[WorkspaceListResponse]] + + workspaces.length shouldBe 2 + workspaces.map(_.workspace.name).toSet shouldBe Set(workspaceName1, workspaceName2) + } finally { + Rawls.workspaces.delete(projectName, workspaceName1) + assertNoAccessToWorkspace(projectName, workspaceName1) + + Rawls.workspaces.delete(projectName, workspaceName2) + assertNoAccessToWorkspace(projectName, workspaceName2) } } it should "allow sharing a workspace" in { - implicit val token = owner.makeAuthToken() - withTemporaryAzureBillingProject(azureManagedAppCoordinates) { projectName => - val workspaceName = generateWorkspaceName() - Rawls.workspaces.create( + implicit val token = ownerAuthToken + val projectName = billingProject + val workspaceName = generateWorkspaceName() + Rawls.workspaces.create( + projectName, + workspaceName, + Set.empty, + Map("disableAutomaticAppCreation" -> "true") + ) + try { + val response = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceName)) + response.workspace.name should be(workspaceName) + response.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) + + val userToken = nonOwnerAuthToken + eventually { + intercept[Exception] { + getSasUrl(projectName, workspaceName, userToken) + } + } + + // Make nonOwner a writer + Orchestration.workspaces.updateAcl( projectName, workspaceName, - Set.empty, - Map("disableAutomaticAppCreation" -> "true") + nonOwnerAuthToken.userData.email, + WorkspaceAccessLevel.Writer, + Some(false), + Some(false) ) - try { - val response = workspaceResponse(Rawls.workspaces.getWorkspaceDetails(projectName, workspaceName)) - response.workspace.name should be(workspaceName) - response.workspace.cloudPlatform should be(Some(WorkspaceCloudPlatform.Azure)) - - // nonOwner is not a member of the workspace, should not be able to write - val userToken = nonOwner.makeAuthToken() - eventually { - intercept[Exception] { - getSasUrl(projectName, workspaceName, userToken) - } - } + // Verify can get a Sas URL to write to workspace + getSasUrl(projectName, workspaceName, userToken) - // Make nonOwner a writer - Orchestration.workspaces.updateAcl( - projectName, - workspaceName, - nonOwner.email, - WorkspaceAccessLevel.Writer, - Some(false), - Some(false) - ) - // Verify can get a Sas URL to write to workspace - getSasUrl(projectName, workspaceName, userToken) - - // Remove write access - Orchestration.workspaces.updateAcl( - projectName, - workspaceName, - nonOwner.email, - WorkspaceAccessLevel.NoAccess, - Some(false), - Some(false) - ) - eventually { - intercept[Exception] { - getSasUrl(projectName, workspaceName, userToken) - } + // Remove write access + Orchestration.workspaces.updateAcl( + projectName, + workspaceName, + nonOwnerAuthToken.userData.email, + WorkspaceAccessLevel.NoAccess, + Some(false), + Some(false) + ) + eventually { + intercept[Exception] { + getSasUrl(projectName, workspaceName, userToken) } - } finally { - Rawls.workspaces.delete(projectName, workspaceName) - assertNoAccessToWorkspace(projectName, workspaceName) } + } finally { + Rawls.workspaces.delete(projectName, workspaceName) + assertNoAccessToWorkspace(projectName, workspaceName) } } diff --git a/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/PipelineInjector.scala b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/PipelineInjector.scala new file mode 100644 index 0000000000..88e5dc2332 --- /dev/null +++ b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/PipelineInjector.scala @@ -0,0 +1,70 @@ +package org.broadinstitute.dsde.test.pipeline + +import com.google.api.client.googleapis.testing.auth.oauth2.MockGoogleCredential +import com.typesafe.scalalogging.LazyLogging +import io.circe.parser._ +import org.broadinstitute.dsde.workbench.auth.AuthToken + +import java.util.Base64 +import scala.util.Random + +object PredefinedEnv { + val BillingProject: String = "BILLING_PROJECT" + val UsersMetadataB64: String = "USERS_METADATA_B64" + val E2EENV: String = "E2E_ENV" +} + +trait PipelineInjector { + // The name of the environment you requested the pipeline to return. + def environmentName: String + + // Returns the billing project name you requested the pipeline to create. + def billingProject: String = + sys.env.getOrElse(PredefinedEnv.BillingProject, "") + + // Retrieves user metadata from the environment and decodes it from Base64. + // Returns a sequence of UserMetadata objects. An empty Seq will be returned if retrieval fails. + def usersMetadata: Seq[UserMetadata] = + sys.env.get(PredefinedEnv.UsersMetadataB64) match { + case Some(b64) => + val decoded = decode[Seq[UserMetadata]](new String(Base64.getDecoder.decode(b64), "UTF-8")) + decoded match { + case Right(u) => u + case Left(error) => Seq() + } + case _ => Seq() + } + + trait Users { + val users: Seq[UserMetadata] + + def getUserCredential(like: String): Option[UserMetadata] = { + val filteredResults = users.filter(_.email.toLowerCase.contains(like.toLowerCase)) + if (filteredResults.isEmpty) None else Some(filteredResults.head) + } + } + + object Owners extends Users { + val users: Seq[UserMetadata] = usersMetadata.filter(_.`type` == Owner) + } + + object Students extends Users { + val users: Seq[UserMetadata] = usersMetadata.filter(_.`type` == Student) + } + + def chooseStudent: Option[UserMetadata] = { + val students = usersMetadata.filter(_.`type` == Student) + if (students.isEmpty) None else Some(students(Random.nextInt(students.length))) + } +} + +object PipelineInjector extends LazyLogging { + def apply(envName: String): PipelineInjector = new PipelineInjector { + override val environmentName: String = envName + } + + def e2eEnv(): String = { + logger.debug("E2E Env: " + sys.env.getOrElse(PredefinedEnv.E2EENV, "")) + sys.env.getOrElse(PredefinedEnv.E2EENV, "") + } +} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/ProxyAuthToken.scala b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/ProxyAuthToken.scala new file mode 100644 index 0000000000..57cfdda103 --- /dev/null +++ b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/ProxyAuthToken.scala @@ -0,0 +1,34 @@ +package org.broadinstitute.dsde.test.pipeline + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential +import com.typesafe.scalalogging.LazyLogging +import org.broadinstitute.dsde.workbench.auth.AuthToken + +/** + * Represents a proxy authentication token that combines user metadata and Google OAuth2 credentials, + * extending a base `AuthToken` class. + * + * This class serves as an stand-in for `AuthToken` in + * https://github.com/broadinstitute/workbench-libs/blob/develop/serviceTest/src/test/scala/org/broadinstitute/dsde/workbench/auth/AuthToken.scala#L26, allowing it to be enhanced + * with user metadata and authentication credentials. + * + * This class enables us to encapsulate user metadata and authentication credentials obtained from + * the pipeline through injection, providing a convenient way to interact with various APIs while asserting the user's + * authorization. + * + * @param userData The user metadata containing email, user type, and authorization information. + * @param credential The Google OAuth2 credentials builder to assert authorization for API calls. + * @extends AuthToken An extension of a base `AuthToken` class provided by workbench library. + */ +case class ProxyAuthToken(userData: UserMetadata, credential: GoogleCredential) extends AuthToken with LazyLogging { + override def buildCredential(): GoogleCredential = { + logger.debug(s"ProxyAuthToken.buildCredential() has been called for user ${userData.email} ...") + credential.setAccessToken(userData.bearer) + logger.debug( + "... with bearer token " + (if (credential.getAccessToken.length > 10) + credential.getAccessToken.take(10 - 3) + "..." + else credential.getAccessToken) + ) + credential + } +} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserMetadata.scala b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserMetadata.scala new file mode 100644 index 0000000000..d3334c84d6 --- /dev/null +++ b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserMetadata.scala @@ -0,0 +1,46 @@ +package org.broadinstitute.dsde.test.pipeline + +import com.google.api.client.googleapis.testing.auth.oauth2.MockGoogleCredential +import io.circe._ +import io.circe.generic.semiauto._ + +/** + * Represents metadata associated with a user. + * + * @param email The email address associated with the user. + * @param type An instance of UserType (e.g., Owner or Student). + * @param bearer The Bearer token to assert authorization. + * + * @example + * {{{ + * // Sample JSON representation of an array of user metadata injected from the pipeline + * [ + * { + * "email": "hermione.owner@quality.firecloud.org", + * "type": "owner", + * "bearer": "yada yada 1" + * }, + * { + * "email": "harry.potter@quality.firecloud.org", + * "type": "student", + * "bearer": "yada yada 2" + * }, + * { + * "email": "ron.weasley@quality.firecloud.org", + * "type": "student", + * "bearer": "yada yada 3" + * } + * ] + * }}} + */ +case class UserMetadata(email: String, `type`: UserType, bearer: String) { + def makeAuthToken: ProxyAuthToken = + ProxyAuthToken(this, (new MockGoogleCredential.Builder()).build()) +} + +/** + * Companion object containing some useful methods for UserMetadata. + */ +object UserMetadata { + implicit val userMetadataDecoder: Decoder[UserMetadata] = deriveDecoder[UserMetadata] +} diff --git a/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserType.scala b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserType.scala new file mode 100644 index 0000000000..28cab0bb17 --- /dev/null +++ b/automation/src/test/scala/org/broadinstitute/dsde/test/pipeline/UserType.scala @@ -0,0 +1,80 @@ +package org.broadinstitute.dsde.test.pipeline + +import io.circe.Decoder + +/** + * Enum-like sealed trait representing the user type. + */ +sealed trait UserType { def title: String } + +/** + * Enum-like user type for title 'Owner', 'Student' + * + * The user types assignment came directly from the original test horde users below. + * + * @example + * + * { + * "admins": { + * "dumbledore": "dumbledore.admin@quality.firecloud.org", + * "voldemort": "voldemort.admin@quality.firecloud.org" + * }, + * "owners": { + * "hermione": "hermione.owner@quality.firecloud.org", + * "sirius": "sirius.owner@quality.firecloud.org", + * "tonks": "tonks.owner@quality.firecloud.org" + * }, + * "curators": { + * "mcgonagall": "mcgonagall.curator@quality.firecloud.org", + * "snape": "snape.curator@quality.firecloud.org", + * "hagrid": "hagrid.curator@quality.firecloud.org", + * "lupin": "lupin.curator@quality.firecloud.org", + * "flitwick": "flitwick.curator@quality.firecloud.org" + * }, + * "authdomains": { + * "fred": "fred.authdomain@quality.firecloud.org", + * "george": "george.authdomain@quality.firecloud.org", + * "bill": "bill.authdomain@quality.firecloud.org", + * "percy": "percy.authdomain@quality.firecloud.org", + * "molly": "molly.authdomain@quality.firecloud.org", + * "arthur": "arthur.authdomain@quality.firecloud.org" + * }, + * "students": { + * "harry": "harry.potter@quality.firecloud.org", + * "ron": "ron.weasley@quality.firecloud.org", + * "lavender": "lavender.brown@quality.firecloud.org", + * "cho": "cho.chang@quality.firecloud.org", + * "oliver": "oliver.wood@quality.firecloud.org", + * "cedric": "cedric.diggory@quality.firecloud.org", + * "crabbe": "vincent.crabbe@quality.firecloud.org", + * "goyle": "gregory.goyle@quality.firecloud.org", + * "dean": "dean.thomas@quality.firecloud.org", + * "ginny": "ginny.weasley@quality.firecloud.org" + * }, + * "temps": { + * "luna": "luna.temp@quality.firecloud.org", + * "neville": "neville.temp@quality.firecloud.org" + * }, + * "notebookswhitelisted": { + * "hermione": "hermione.owner@quality.firecloud.org", + * "ron": "ron.weasley@quality.firecloud.org" + * }, + * "campaignManagers": { + * "dumbledore": "dumbledore.admin@quality.firecloud.org", + * "voldemort": "voldemort.admin@quality.firecloud.org" + * } + * } + */ +case object Owner extends UserType { def title = "owner" } +case object Student extends UserType { def title = "student" } + +/** + * Companion object containing some useful methods for UserType. + */ +object UserType { + implicit val userTypeDecoder: Decoder[UserType] = Decoder.decodeString.emap { + case "owner" => Right(Owner) + case "student" => Right(Student) + case other => Left(s"Unknown user type: $other") + } +}