diff --git a/Jenkinsfile_CNP b/Jenkinsfile_CNP index f34fefcac4..32d7570973 100644 --- a/Jenkinsfile_CNP +++ b/Jenkinsfile_CNP @@ -140,9 +140,11 @@ withPipeline(type, product, component) { } else { env.DEFINITION_STORE_URL_BASE = "https://ccd-definition-store-ccd-data-store-api-${env.BRANCH_NAME}.preview.platform.hmcts.net".toLowerCase() env.DEFINITION_STORE_HOST = env.DEFINITION_STORE_URL_BASE + env.GROUP_ACCESS_ENABLED = "true" } echo "ES FTA Enabled = ${env.ELASTIC_SEARCH_FTA_ENABLED} on branch ${env.BRANCH_NAME}" + echo "Group Access FT Enabled = ${env.GROUP_ACCESS_ENABLED ?: 'false'}" syncBranchesWithMaster(branchesToSync) overrideVaultEnvironments(vaultOverrides) diff --git a/README.md b/README.md index 38ce26b6ec..a2cf5e511e 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The following environment variables are optional: | Name | Default | Description | |----------------------------------------------|--------------------|-----------------------------------------------| | ELASTIC_SEARCH_FTA_ENABLED | true/false/not set | Enable ElasticSearch FTA (Functional Tests). | -| GROUP_ACCESS_ENABLED | true/false/not set | Enable group access Tesing (Funtional Tests). | +| GROUP_ACCESS_ENABLED | true/false/not set | Enable group access testing (functional tests). | | ENABLE_CASE_GROUP_ACCESS_FILTERING | true/false | Enable case group access filtering. | ### Building @@ -135,6 +135,60 @@ Will run only S-1023.5: ./gradlew functional -P tags="@S-1023.5" ``` +For remote AAT runs, create or refresh the app runtime file with `./gradlew reloadEnvSecrets -Penv=aat`, +then source `./scripts/aat-env.sh` before running `runRemoteAAT` or `smoke`. The default profile fetches only +the required smoke/base secrets and prints `GROUP_ACCESS_ENABLED`. Set `AAT_ENV_MODE=full` before the full functional +suite. + +```bash +az login +./gradlew reloadEnvSecrets -Penv=aat +source ./scripts/aat-env.sh +./scripts/check-remote-env.sh aat +./gradlew runRemoteAAT +./gradlew --no-daemon smoke + +AAT_ENV_MODE=full source ./scripts/aat-env.sh +./gradlew --no-daemon functional +``` + +By default, leave `AAT_DEFINITION_STORE_HOST` unset. Data-store then uses the AAT definition-store URL from +`.aat-remote-env`. + +```bash +unset AAT_DEFINITION_STORE_HOST +source ./scripts/aat-env.sh +./gradlew runRemoteAAT +``` + +To override this and run `ccd-data-store-api` against a local `ccd-definition-store-api` on port `4451`, start +definition-store against AAT with group access enabled, then set `AAT_DEFINITION_STORE_HOST` for data-store: + +```bash +cd ../ccd-definition-store-api +az login +./gradlew reloadEnvSecrets -Penv=aat +grep '^ENABLE_CASE_GROUP_ACCESS=' .aat-remote-env +./scripts/check-remote-env.sh aat +./gradlew runRemoteAat +``` + +Expected: + +```text +ENABLE_CASE_GROUP_ACCESS=true +``` + +```bash +cd ../ccd-data-store-api +export AAT_DEFINITION_STORE_HOST=http://localhost:4451 +source ./scripts/aat-env.sh +./scripts/check-remote-env.sh aat +./gradlew runRemoteAAT +``` + +See `scripts/README.md` for the full data-store remote-run notes and troubleshooting table. + ## LICENSE This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. diff --git a/build.gradle b/build.gradle index 78105c6d09..859233b198 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import groovy.xml.XmlParser +import java.nio.charset.StandardCharsets plugins { id 'maven-publish' @@ -42,6 +43,10 @@ ext { elasticVersion = '9.2.4' testContainersVersion = '2.0.5' cucumber = '7.34.3' + cucumberMessages = '30.1.0' + cucumberQuery = '14.7.0' + beftaCucumberMessages = '24.1.0' + beftaCucumberQuery = '12.2.0' beftaFwVersion = '9.2.6' ccdTestDefinitionVersion = '7.26.7' limits = [ @@ -187,6 +192,43 @@ configurations { } } +configurations.matching { + it.name in [ + 'testCompileClasspath', + 'testRuntimeClasspath', + 'aatCompileClasspath', + 'aatRuntimeClasspath', + 'contractTestCompileClasspath', + 'contractTestRuntimeClasspath', + 'integrationTestCompileClasspath', + 'integrationTestRuntimeClasspath' + ] +}.configureEach { + resolutionStrategy.eachDependency { details -> + if (details.requested.group == 'io.cucumber' && details.requested.name == 'messages') { + details.useVersion beftaCucumberMessages + details.because 'BEFTA Cucumber dependencies declare io.cucumber:messages as a version range, which can fail dynamic resolution' + } + if (details.requested.group == 'io.cucumber' && details.requested.name == 'query') { + details.useVersion beftaCucumberQuery + details.because 'BEFTA Cucumber XML formatters declare io.cucumber:query as a version range, which can fail dynamic resolution' + } + } +} + +configurations.matching { it.name == 'cucumberRuntime' }.configureEach { + resolutionStrategy.eachDependency { details -> + if (details.requested.group == 'io.cucumber' && details.requested.name == 'messages') { + details.useVersion cucumberMessages + details.because 'Cucumber runtime dependencies declare io.cucumber:messages as a version range, which can fail dynamic resolution' + } + if (details.requested.group == 'io.cucumber' && details.requested.name == 'query') { + details.useVersion cucumberQuery + details.because 'Cucumber runtime formatters declare io.cucumber:query as a version range, which can fail dynamic resolution' + } + } +} + configurations.matching { it.name in ['aatCompileClasspath', 'aatRuntimeClasspath', 'cucumberRuntime'] }.configureEach { resolutionStrategy.eachDependency { details -> if (details.requested.group?.startsWith('com.fasterxml.jackson')) { @@ -734,7 +776,6 @@ jacocoTestReport { test { timeout = Duration.ofMinutes(30) environment("AZURE_APPLICATIONINSIGHTS_INSTRUMENTATIONKEY", "some-key") - generateCucumberReports.enabled = true systemProperty 'java.locale.providers', 'CLDR' useJUnitPlatform() @@ -809,7 +850,6 @@ task smoke(type: JavaExec) { into "$buildDir/test-results/test" } - generateCucumberReports.enabled = true mainClass = "uk.gov.hmcts.ccd.datastore.befta.DataStoreBeftaMain" classpath += configurations.cucumberRuntime + sourceSets.aat.runtimeClasspath args = ['--plugin', "json:${rootDir}/target/cucumber.json", @@ -819,16 +859,7 @@ task smoke(type: JavaExec) { '--glue', "uk.gov.hmcts.ccd.datastore.befta", 'src/aat/resources/features'] jvmArgs = [ '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED' ] - finalizedBy { - generateCucumberReports { - doLast{ - delete "${rootDir}/BEFTA Report for Smoke Tests/" - new File("${rootDir}/BEFTA Report for Smoke Tests").mkdirs() - file("${rootDir}/target/cucumber/cucumber-html-reports").renameTo(file("${rootDir}/BEFTA Report for Smoke Tests")) - logger.quiet("Smoke test report moved to ---> file://${rootDir}/BEFTA%20Report%20for%20Smoke%20Tests/overview-features.html") - } - } - } + finalizedBy 'moveSmokeCucumberReport' outputs.upToDateWhen { false } } @@ -844,7 +875,6 @@ task functional(type: JavaExec) { group = "Verification" - generateCucumberReports.enabled = false mainClass = "uk.gov.hmcts.ccd.datastore.befta.DataStoreBeftaMain" classpath += configurations.cucumberRuntime + sourceSets.aat.runtimeClasspath + sourceSets.main.output + sourceSets.test.output args = [ @@ -860,17 +890,7 @@ task functional(type: JavaExec) { // '--add-opens=...' added to suppress 'WARNING: An illegal reflective access operation has occurred' in uk.gov.hmcts.befta.util.CucumberStepAnnotationUtils jvmArgs '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED' - finalizedBy { - generateCucumberReports.enabled = true - generateCucumberReports { - doLast{ - delete "${rootDir}/BEFTA Report for Functional Tests/" - new File("${rootDir}/BEFTA Report for Functional Tests").mkdirs() - file("${rootDir}/target/cucumber/cucumber-html-reports").renameTo(file("${rootDir}/BEFTA Report for Functional Tests")) - logger.quiet("Functional test report moved to ---> file://${rootDir}/BEFTA%20Report%20for%20Functional%20Tests/overview-features.html") - } - } - } + finalizedBy 'moveFunctionalCucumberReport' outputs.upToDateWhen { false } } @@ -903,28 +923,82 @@ def timestamp() { return date.format('yyyy-MM-dd HH:mm:ss') } +def cucumberJsonReport = file("${projectDir}/target/cucumber.json") + cucumberReports { outputDir = file("${projectDir}/target/cucumber") - reports = files("${projectDir}/target/cucumber.json") + reports = files(cucumberJsonReport) notFailingStatuses = ["skipped", "passed"] } +tasks.named('generateCucumberReports') { + onlyIf { + cucumberJsonReport.exists() + && (gradle.taskGraph.hasTask(':smoke') + || gradle.taskGraph.hasTask(':functional') + || gradle.taskGraph.hasTask(':moveSmokeCucumberReport') + || gradle.taskGraph.hasTask(':moveFunctionalCucumberReport') + || gradle.startParameter.taskNames.any { + it == 'generateCucumberReports' || it.endsWith(':generateCucumberReports') + }) + } +} + +tasks.register('moveSmokeCucumberReport') { + dependsOn 'generateCucumberReports' + + doLast { + moveCucumberReport('Smoke Tests') + } +} + +tasks.register('moveFunctionalCucumberReport') { + dependsOn 'generateCucumberReports' + + doLast { + moveCucumberReport('Functional Tests') + } +} + +void moveCucumberReport(String reportName) { + def source = file("${rootDir}/target/cucumber/cucumber-html-reports") + def destination = file("${rootDir}/BEFTA Report for ${reportName}") + + if (!source.exists()) { + throw new GradleException("Cucumber HTML report was not generated at ${source}") + } + + delete destination + copy { + from source + into destination + } + delete source + + logger.quiet("${reportName} report moved to ---> file://${rootDir}/BEFTA%20Report%20for%20${reportName.replace(' ', '%20')}/overview-features.html") +} + +task reloadEnvSecrets { + doFirst { + def env = project.findProperty('env') ?: 'demo' + reloadRemoteEnvSecrets(env.toString()) + } +} + task reloadEnvSecretsDemo { doFirst { - reloadEnvSecrets("demo") + reloadRemoteEnvSecrets("demo") } } task reloadEnvSecretsAAT { doFirst { - reloadEnvSecrets("aat") + reloadRemoteEnvSecrets("aat") } } -void reloadEnvSecrets(String env) { - if (project.file("./.${env}-remote-env").exists()) { - project.file("./.${env}-remote-env").delete() - } +void reloadRemoteEnvSecrets(String env) { + loadEnvSecrets(env, true) } task runRemoteDemo(type: JavaExec) { @@ -952,28 +1026,98 @@ tasks.named("processContractTestResources") { } void configRemoteRunTask(Task execTask, String env) { - loadEnvSecrets(env) - project.file("./.${env}-remote-env").readLines().each() { + def envFile = loadEnvSecrets(env) + envFile.readLines().each() { def index = it.indexOf("=") def key = it.substring(0, index) def value = it.substring(index + 1) execTask.environment(key, value) } + applyRemoteRunOverrides(execTask) } -void loadEnvSecrets(String env) { +void applyRemoteRunOverrides(Task execTask) { + def definitionStoreHostOverride = project.findProperty('definitionStoreHost') ?: System.getenv('AAT_DEFINITION_STORE_HOST') + def caseGroupAccessFiltering = project.findProperty('enableCaseGroupAccessFiltering') + ?: System.getenv('ENABLE_CASE_GROUP_ACCESS_FILTERING') + ?: System.getenv('GROUP_ACCESS_ENABLED') + + if (definitionStoreHostOverride) { + execTask.environment('DEFINITION_STORE_HOST', definitionStoreHostOverride.toString()) + execTask.environment('DEFINITION_STORE_URL_BASE', definitionStoreHostOverride.toString()) + execTask.environment('ENABLE_CASE_GROUP_ACCESS_FILTERING', (caseGroupAccessFiltering ?: 'true').toString()) + logger.lifecycle("Using definition-store override: ${definitionStoreHostOverride}") + logger.lifecycle("Using case group access filtering: ${(caseGroupAccessFiltering ?: 'true')}") + } else if (caseGroupAccessFiltering) { + execTask.environment('ENABLE_CASE_GROUP_ACCESS_FILTERING', caseGroupAccessFiltering.toString()) + } +} + +File loadEnvSecrets(String env) { + return loadEnvSecrets(env, isRemoteEnvRefreshRequested()) +} + +File loadEnvSecrets(String env, boolean refreshRemoteEnv) { def azCmd = ['az', 'keyvault', 'secret', 'show', '--vault-name', "ccd-${env}", '-o', 'tsv', '--query', 'value', '--name', 'data-store-remote-env'] - if (!project.file("./.${env}-remote-env").exists()) { - new ByteArrayOutputStream().withStream { os -> - exec { - if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) { - commandLine ['cmd', '/c'] + azCmd - } else { - commandLine azCmd - } - standardOutput = os + def envFile = project.file("./.${env}-remote-env") + def targetEnvFile = project.file("./target/.${env}-remote-env") + + if (!refreshRemoteEnv && envFile.exists()) { + ensureRemoteEnvDefaults(envFile, env) + return envFile + } + if (!refreshRemoteEnv && targetEnvFile.exists()) { + ensureRemoteEnvDefaults(targetEnvFile, env) + return targetEnvFile + } + + try { + def encodedSecrets = providers.exec { + if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) { + commandLine ['cmd', '/c'] + azCmd + } else { + commandLine azCmd } - project.file("./.${env}-remote-env").write(new String(os.toString().replace('\n', '').decodeBase64(), StandardCharsets.UTF_8)) + }.standardOutput.asText.get() + + envFile.write(new String(encodedSecrets.replace('\n', '').replace('\r', '').decodeBase64(), StandardCharsets.UTF_8)) + ensureRemoteEnvDefaults(envFile, env) + return envFile + } catch (Exception e) { + if (refreshRemoteEnv) { + throw new GradleException("Failed to refresh ${envFile.name}", e) + } + if (envFile.exists()) { + logger.warn("Failed to refresh ${envFile.name}; using the existing file. Cause: ${e.message}") + ensureRemoteEnvDefaults(envFile, env) + return envFile } + if (targetEnvFile.exists()) { + logger.warn("Failed to refresh ${envFile.name}; using ${targetEnvFile}. Cause: ${e.message}") + ensureRemoteEnvDefaults(targetEnvFile, env) + return targetEnvFile + } + throw e + } +} + +void ensureRemoteEnvDefaults(File envFile, String env) { + if (!remoteEnvContainsKey(envFile, 'DATA_STORE_DB_HOST')) { + appendRemoteEnvValue(envFile, 'DATA_STORE_DB_HOST', "ccd-data-store-api-postgres-db-v15-${env}.postgres.database.azure.com") } } + +boolean remoteEnvContainsKey(File envFile, String key) { + envFile.readLines().any { it.startsWith("${key}=") } +} + +void appendRemoteEnvValue(File envFile, String key, String value) { + def contents = envFile.getText(StandardCharsets.UTF_8.name()) + def linePrefix = contents.isEmpty() || contents.endsWith('\n') ? '' : System.lineSeparator() + envFile.append("${linePrefix}${key}=${value}${System.lineSeparator()}", StandardCharsets.UTF_8.name()) +} + +boolean isRemoteEnvRefreshRequested() { + def refreshRemoteEnv = project.findProperty('refreshRemoteEnv') ?: System.getProperty('refreshRemoteEnv') + return refreshRemoteEnv != null && refreshRemoteEnv.toString().toBoolean() +} diff --git a/charts/ccd-data-store-api/values.preview.template.yaml b/charts/ccd-data-store-api/values.preview.template.yaml index f8c1854b9d..8be1c9140c 100644 --- a/charts/ccd-data-store-api/values.preview.template.yaml +++ b/charts/ccd-data-store-api/values.preview.template.yaml @@ -94,7 +94,7 @@ ccd: ccd-definition-store-api: java: ingressHost: ccd-definition-store-${SERVICE_FQDN} - image: hmctsprod.azurecr.io/ccd/definition-store-api:latest + image: hmctsprod.azurecr.io/ccd/definition-store-api:pr-1815 imagePullPolicy: Always devmemoryRequests: 2048Mi devcpuRequests: 2000m @@ -110,6 +110,7 @@ ccd: ELASTIC_SEARCH_ENABLED: true ELASTIC_SEARCH_HOST: "{{ .Release.Name }}-es-master" ELASTIC_SEARCH_SCHEME: http + ENABLE_CASE_GROUP_ACCESS: true secrets: DEFINITION_STORE_DB_PASSWORD: secretRef: "{{ .Values.global.postgresSecret }}" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..cdf722b985 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,268 @@ +# AAT Remote Run Scripts + +These scripts support local `ccd-data-store-api` runs against AAT dependencies. + +## Remote App Environment + +`runRemoteAAT` and `runRemoteDemo` load local runtime settings from files named `.-remote-env`. + +```bash +az login +./gradlew reloadEnvSecrets -Penv=aat +./gradlew reloadEnvSecrets -Penv=demo +``` + +| Environment | File created | Run task | +|-------------|--------------|----------| +| `aat` | `.aat-remote-env` | `./gradlew runRemoteAAT` | +| `demo` | `.demo-remote-env` | `./gradlew runRemoteDemo` | + +The `env` value maps to the Key Vault `ccd-` and the secret `data-store-remote-env`. +The files are ignored by Git because they contain local runtime settings and secrets. + +Source `aat-env.sh` before running AAT-backed smoke tests. It exports the BEFTA/IDAM/S2S values used by the test +runner from the same AAT Key Vault secrets used by Jenkins. The default profile fetches only the required smoke/base +secrets, then prints a short summary including `GROUP_ACCESS_ENABLED`. + +```bash +source ./scripts/aat-env.sh +``` + +Use `AAT_ENV_MODE=full` when running the full functional suite. It fetches the additional optional private, +restricted, and cross-case-type BEFTA users. + +```bash +AAT_ENV_MODE=full source ./scripts/aat-env.sh +``` + +If the current shell already has AAT secrets exported, the script reuses them. Force fresh Key Vault reads with: + +```bash +AAT_ENV_REFRESH_SECRETS=true source ./scripts/aat-env.sh +``` + +Show the sourceable script help without fetching secrets: + +```bash +source ./scripts/aat-env.sh help +``` + +## Definition Store Target + +By default, leave `AAT_DEFINITION_STORE_HOST` unset. Data-store then uses the AAT definition-store URL from +`.aat-remote-env`. + +```bash +unset AAT_DEFINITION_STORE_HOST +source ./scripts/aat-env.sh +./scripts/check-remote-env.sh aat +./gradlew runRemoteAAT +``` + +### Local Definition Store + +Use `AAT_DEFINITION_STORE_HOST` when data-store should use a locally running `ccd-definition-store-api` instead of the +AAT definition-store service. The local definition-store must itself be running against AAT, otherwise data-store will +mix AAT data-store dependencies with local definition-store dependencies. + +In `ccd-definition-store-api`, start the local definition-store app against AAT first: + +```bash +cd ../ccd-definition-store-api +az login +./gradlew reloadEnvSecrets -Penv=aat +grep '^ENABLE_CASE_GROUP_ACCESS=' .aat-remote-env +./scripts/check-remote-env.sh aat +./gradlew runRemoteAat +``` + +Expected: + +```text +ENABLE_CASE_GROUP_ACCESS=true +``` + +Then, in `ccd-data-store-api`, point both the app and BEFTA setup at that local definition-store. The override is +applied to both `DEFINITION_STORE_HOST` and `DEFINITION_STORE_URL_BASE`. + +```bash +cd ../ccd-data-store-api +export AAT_DEFINITION_STORE_HOST=http://localhost:4451 +source ./scripts/aat-env.sh +./scripts/check-remote-env.sh aat +./gradlew runRemoteAAT +``` + +With `AAT_DEFINITION_STORE_HOST` set, `check-remote-env.sh` checks `http://localhost:4451/health` as well as the +remaining AAT dependencies. The AAT backing for definition-store comes from starting it with `runRemoteAat` and an +`.aat-remote-env` that contains `ENABLE_CASE_GROUP_ACCESS=true`. + +This override only changes definition-store. Data-store and definition-store still need AAT internal services such as +S2S, role-assignment, Elasticsearch, and the AAT databases. + +For a one-off Gradle app run without exporting the variable: + +```bash +./gradlew runRemoteAAT -PdefinitionStoreHost=http://localhost:4451 +``` + +That one-off form also defaults `ENABLE_CASE_GROUP_ACCESS_FILTERING=true`. Override it explicitly only when needed: + +```bash +./gradlew runRemoteAAT \ + -PdefinitionStoreHost=http://localhost:4451 \ + -PenableCaseGroupAccessFiltering=false +``` + +### Group Access Flags + +When running group-access functional tests, these values must be set in the process that uses them. `aat-env.sh` +defaults `GROUP_ACCESS_ENABLED` and `ENABLE_CASE_GROUP_ACCESS_FILTERING` to `true` for this AAT flow. + +| Variable | Process | Purpose | Expected value | +|----------|---------|---------|----------------| +| `GROUP_ACCESS_ENABLED` | BEFTA functional test terminal | Allows `@groupaccess` scenarios to run. | `true` | +| `ENABLE_CASE_GROUP_ACCESS_FILTERING` | `ccd-data-store-api` app terminal | Makes data-store add and maintain `CaseAccessGroups` in case data and data classification. | `true` | +| `ENABLE_CASE_GROUP_ACCESS` | `ccd-definition-store-api` app terminal | Makes definition-store expose the group-access definition behaviour needed by these tests. | `true` | + +Values are read when each process starts. Restart the relevant app after changing a value. + +```bash +echo "GROUP_ACCESS_ENABLED=${GROUP_ACCESS_ENABLED}" +echo "ENABLE_CASE_GROUP_ACCESS_FILTERING=${ENABLE_CASE_GROUP_ACCESS_FILTERING}" +./gradlew runRemoteAAT -PdefinitionStoreHost=http://localhost:4451 -PenableCaseGroupAccessFiltering=true +``` + +### Verify Group Access Definitions + +For F-1026, the generated test definitions must be loaded into the running definition-store, not only present under +`build/tmp`. The expected extracted files are: + +| File | Expected content | +|------|------------------| +| `build/tmp/BEFTA_MASTER_GROUPACCESS/common/CaseType.json` | `FT_CaseProfessionalGroupAccess` | +| `build/tmp/BEFTA_MASTER_GROUPACCESS/FT_CaseProfessionalGroupAccess/CaseField.json` | `CaseAccessGroups` and `OrganisationPolicyField` | +| `build/tmp/BEFTA_MASTER_GROUPACCESS/common/AccessTypeRole.json` | `CaseAssignedRoleField=CaseProfessionalGroupAccess_GA_Role` and `CaseAccessGroupIDTemplate=BEFTA_MASTER:FT_CaseProfessionalGroupAccess:CaseProfessionalGroupAccess_GA_Role:$ORGID$` | + +If a F-1026 create-case response returns `201` and removes the submitted `CCD:all-cases-access` entry, then +`ENABLE_CASE_GROUP_ACCESS_FILTERING=true` is active in data-store. If data-store does not add the replacement +`CaseAccessGroups` value, the full case type returned by definition-store is missing the matching `AccessTypeRole` +metadata, or data-store has cached an older case type definition. + +Load the group-access definitions into the running local definition-store before starting data-store: + +```bash +cd ../ccd-definition-store-api +ENABLE_CASE_GROUP_ACCESS=true GROUP_ACCESS_ENABLED=true ./gradlew runRemoteAat +``` + +In a second terminal, run the data setup from this repo so BEFTA imports this repo's test definitions into the +definition-store instance on `localhost:4451`: + +```bash +cd ../ccd-data-store-api +export AAT_DEFINITION_STORE_HOST=http://localhost:4451 +source ./scripts/aat-env.sh +./gradlew highLevelDataSetup --args='aat' +``` + +Running a targeted functional scenario also performs the same BEFTA definition import before the scenario runs: + +```bash +GROUP_ACCESS_ENABLED=true ./gradlew functional -Ptags='@S-1026.1' +``` + +| Check | Expected result | Action if it fails | +|-------|-----------------|--------------------| +| `ccd-definition-store-api` started with `ENABLE_CASE_GROUP_ACCESS=true` | Definition-store exposes group-access metadata in case type responses. | Restart definition-store with the flag set before running data-store. | +| `ccd-data-store-api` started after definition-store is ready | Data-store reads the group-access case type after metadata is available. | Restart data-store to clear the cached case type definition. | +| Data-store startup uses `-PdefinitionStoreHost=http://localhost:4451 -PenableCaseGroupAccessFiltering=true` | `DEFINITION_STORE_HOST`, `DEFINITION_STORE_URL_BASE`, and `ENABLE_CASE_GROUP_ACCESS_FILTERING` are set for the app process. | Restart data-store with those properties, or export the equivalent env vars before startup. | +| Local definition-store response for `FT_CaseProfessionalGroupAccess` contains `accessTypeRoles` for `CaseProfessionalGroupAccess_GA_Role` | Data-store has the metadata needed to generate `BEFTA_MASTER:FT_CaseProfessionalGroupAccess:CaseProfessionalGroupAccess_GA_Role:orgID1`. | Re-run `./gradlew highLevelDataSetup --args='aat'` from this repo, or run the targeted functional scenario so BEFTA imports `BEFTA_MASTER_GROUPACCESS`. | + +### AAT S2S URL + +For this repo's normal AAT flow, `runRemoteAAT` runs directly on the host machine, not inside Docker. In that setup, +`host.docker.internal` is the wrong S2S host. + +| Runtime setup | S2S URL value | Valid for this AAT flow | +|---------------|---------------|-------------------------| +| Gradle app running on the host with AAT private DNS/VPN | `http://rpe-service-auth-provider-aat.service.core-compute-aat.internal` | Yes | +| Gradle app running on the host with a local S2S tunnel | `http://localhost:` | Yes, only when the tunnel is running | +| App running inside Docker and calling a tunnel on the host machine | `http://host.docker.internal:` | No for `runRemoteAAT`; only valid for a Dockerised app process | + +`./scripts/check-remote-env.sh aat` accepts the AAT internal S2S hostname, or `localhost`/`127.0.0.1` for an intentional +host-machine S2S tunnel. It fails fast if `IDAM_S2S_URL` or `S2S_URL_BASE` points at `host.docker.internal`, because +that usually means a Docker-only tunnel value has leaked into the host-based AAT run. + +### AAT Elasticsearch URLs + +For AAT, data-store reads Elasticsearch nodes from `ELASTIC_SEARCH_HOSTS` or `ELASTIC_SEARCH_DATA_NODES_HOSTS`. +`check-remote-env.sh` checks every configured node. + +| Runtime setup | Elasticsearch URL value | Valid for this AAT flow | +|---------------|-------------------------|-------------------------| +| Gradle app running on the host with AAT private DNS/VPN | `http://ccd-data-.service.core-compute-aat.internal:9200` | Yes | +| Gradle app running on the host with a local Elasticsearch tunnel | `http://localhost:` | Yes, only when the tunnel is running | +| App running inside Docker and calling a tunnel on the host machine | `http://host.docker.internal:` | No for `runRemoteAAT`; only valid for a Dockerised app process | + +`./scripts/check-remote-env.sh aat` accepts AAT `ccd-data-*` nodes, or `localhost`/`127.0.0.1` for an intentional +host-machine Elasticsearch tunnel. It fails fast if Elasticsearch is configured with `host.docker.internal`. + +### Troubleshooting + +| Symptom | Meaning | Action | +|---------|---------|--------| +| `UnknownHostException: rpe-service-auth-provider-aat.service.core-compute-aat.internal` | The local shell or app cannot resolve private AAT DNS. | Connect to the required HMCTS VPN/bastion/private DNS route, then rerun `./scripts/check-remote-env.sh aat` in both repos. | +| `FAIL s2s: AAT IDAM_S2S_URL/S2S_URL_BASE uses http://host.docker.internal:` | A Docker-only host alias is configured for the host-based AAT run. | Use the AAT internal S2S hostname, or use `http://localhost:` if a local S2S tunnel is intentionally running on the host. | +| `FAIL elastic search: AAT ELASTIC_SEARCH_HOSTS/ELASTIC_SEARCH_DATA_NODES_HOSTS uses http://host.docker.internal:` | A Docker-only host alias is configured for the host-based AAT run. | Use the AAT `ccd-data-*` Elasticsearch hosts, or use `http://localhost:` if a local Elasticsearch tunnel is intentionally running on the host. | +| `401` from `http://localhost:4451/api/data/case-type//version` | The request reached local definition-store, but definition-store rejected data-store's forwarded AAT `Authorization` or `ServiceAuthorization` headers. | Check definition-store logs at the same timestamp. Confirm it was started with `runRemoteAat`, can reach AAT S2S, and allows the `ccd_data` S2S service. | +| `Timeout connecting to ccd-data-.service.core-compute-aat.internal:9200` or `Exception executing Elasticsearch search: Connection reset` from `/internal/searchCases` | DNS resolved, but data-store could not open or keep a TCP connection to an AAT Elasticsearch data node. | Run `./scripts/check-remote-env.sh aat`. It checks every value in `ELASTIC_SEARCH_HOSTS` or `ELASTIC_SEARCH_DATA_NODES_HOSTS`; all configured nodes should be reachable on port `9200`. If only one node fails, refresh `.aat-remote-env` and check whether that AAT node is down or unreachable from the VPN/bastion route. | +| `actualResponse.body.case_data.CaseAccessGroups is unavailable` or `actualResponse.body.data_classification.CaseAccessGroups is unavailable` in F-1023/F-1026 | BEFTA ran the scenario. If the submitted `CCD:all-cases-access` entry was removed, data-store filtering is active but no matching `AccessTypeRole` metadata was available. | Use the group-access definition checks above. Restart definition-store first with `ENABLE_CASE_GROUP_ACCESS=true`, then restart data-store with `-PdefinitionStoreHost=http://localhost:4451 -PenableCaseGroupAccessFiltering=true` so it does not use a stale cached case type. | + +If `DATA_STORE_DB_HOST` is missing from the Key Vault value, the Gradle helper adds the standard host for the selected +environment: + +```text +ccd-data-store-api-postgres-db-v15-.postgres.database.azure.com +``` + +`DEFINITION_STORE_HOST` should continue to point at `ccd-definition-store-api`; data-store depends on definition-store +for case definitions. + +## Connectivity Check + +Before starting the app, check DNS and TCP connectivity to the remote dependencies: + +```bash +./scripts/check-remote-env.sh aat +``` + +The check reads `.aat-remote-env` and tests data-store dependencies including Postgres, S2S, IDAM, definition-store, +user-profile, case-document, role-assignment, location reference, and Elasticsearch. + +Failures usually mean the local shell is not connected to the required VPN/bastion route, or the remote env file is +missing a required setting. + +Start the application in one terminal: + +```bash +source ./scripts/aat-env.sh +./gradlew runRemoteAAT +``` + +Run tests in a second terminal: + +```bash +az login +source ./scripts/aat-env.sh +./gradlew --stop +./gradlew --no-daemon smoke + +AAT_ENV_MODE=full source ./scripts/aat-env.sh +./gradlew --no-daemon functional +``` + +```text +AAT environment exported: + GROUP_ACCESS_ENABLED=true +``` diff --git a/scripts/aat-env.sh b/scripts/aat-env.sh new file mode 100755 index 0000000000..78137b1416 --- /dev/null +++ b/scripts/aat-env.sh @@ -0,0 +1,571 @@ +#!/usr/bin/env bash +# +# Source this before running data-store smoke or functional tests against AAT dependencies. +# +# Local app run: +# source ./scripts/aat-env.sh +# ./gradlew runRemoteAAT +# ./gradlew --no-daemon smoke +# +# Full functional run: +# AAT_ENV_MODE=full source ./scripts/aat-env.sh +# ./gradlew --no-daemon functional +# +# Deployed AAT target: +# CCD_DATA_STORE_TARGET=remote source ./scripts/aat-env.sh +# ./gradlew --no-daemon smoke +# +# Local definition-store override: +# export AAT_DEFINITION_STORE_HOST=http://localhost:4451 +# source ./scripts/aat-env.sh +# +# Requires Azure CLI access to ccd-aat and s2s-aat Key Vaults. + +is_true() { + case "${1:-}" in + true|TRUE|True|1|yes|YES|Yes|y|Y|on|ON|On) + return 0 + ;; + *) + return 1 + ;; + esac +} + +should_refresh_secrets() { + is_true "${AAT_ENV_REFRESH_SECRETS:-false}" +} + +env_var_has_value() { + local variable_name="$1" + + eval '[ -n "${'"${variable_name}"':-}" ]' +} + +all_env_vars_have_values() { + local variable_name + + for variable_name in "$@"; do + env_var_has_value "${variable_name}" || return 1 + done +} + +aat_env_secret_profile() { + local requested_profile="${1:-${AAT_ENV_MODE:-smoke}}" + + case "${requested_profile}" in + ""|smoke|default) + printf '%s' "smoke" + ;; + functional|full|all) + printf '%s' "full" + ;; + *) + echo "ERROR: Unknown AAT env mode '${requested_profile}'. Use 'smoke' or 'full'." >&2 + return 1 + ;; + esac +} + +print_aat_env_help() { + cat <<'EOF' +Usage: + source ./scripts/aat-env.sh + AAT_ENV_MODE=full source ./scripts/aat-env.sh + AAT_ENV_REFRESH_SECRETS=true source ./scripts/aat-env.sh + source ./scripts/aat-env.sh help + +Modes: + smoke Default. Fetches only the required smoke/base BEFTA secrets. + full Fetches smoke/base secrets plus optional functional BEFTA users. + +Environment: + AAT_ENV_MODE smoke or full. Positional 'functional'/'full' is also accepted. + AAT_ENV_REFRESH_SECRETS true to discard already-exported AAT secret values before fetching. + AAT_DEFINITION_STORE_HOST optional local definition-store override, for example http://localhost:4451. + Leave unset to use DEFINITION_STORE_HOST from .aat-remote-env. + CCD_DATA_STORE_TARGET local by default, or remote/aat for the deployed AAT service URL. + GROUP_ACCESS_ENABLED defaults to true and is printed in the export summary. + ENABLE_CASE_GROUP_ACCESS_FILTERING defaults to GROUP_ACCESS_ENABLED. + +App runtime values still come from .aat-remote-env. Refresh that file with: + ./gradlew reloadEnvSecrets -Penv=aat +EOF +} + +require_az_cli() { + if ! command -v az >/dev/null 2>&1; then + echo "ERROR: Azure CLI 'az' was not found on PATH." >&2 + echo "Install Azure CLI, then run 'az login' before sourcing this script." >&2 + return 1 + fi +} + +require_azure_keyvault_token() { + local account_output + local token_output + + if ! account_output="$(az account show --query '{name:name, tenantId:tenantId, user:user.name}' -o json 2>&1)"; then + echo "ERROR: Azure CLI is not logged in or cannot load the current account." >&2 + echo "${account_output}" >&2 + echo "Run 'az login', then retry: source ./scripts/aat-env.sh" >&2 + return 1 + fi + + if ! token_output="$(az account get-access-token --resource https://vault.azure.net --query expiresOn -o tsv 2>&1)"; then + echo "ERROR: Azure CLI cannot get a Key Vault access token." >&2 + echo "${token_output}" >&2 + echo "If the error mentions login.microsoftonline.com, fix Azure CLI internet/proxy/VPN access first." >&2 + echo "Useful check: az account get-access-token --resource https://vault.azure.net -o table" >&2 + return 1 + fi +} + +kv_secret() { + local vault_name="$1" + local secret_name="$2" + local secret_value + + if ! secret_value="$(az keyvault secret show \ + --vault-name "${vault_name}" \ + --name "${secret_name}" \ + --query value \ + -o tsv)"; then + return 1 + fi + + secret_value="${secret_value//$'\r'/}" + secret_value="${secret_value//$'\n'/}" + printf '%s' "${secret_value}" +} + +export_secret() { + local variable_name="$1" + local vault_name="$2" + local secret_name="$3" + local secret_value + + if ! should_refresh_secrets && env_var_has_value "${variable_name}"; then + return 0 + fi + + if ! secret_value="$(kv_secret "${vault_name}" "${secret_name}")"; then + echo "ERROR: Failed to read '${secret_name}' from Key Vault '${vault_name}'." >&2 + echo "Confirm your Azure account has access to the AAT Key Vaults, then retry." >&2 + return 1 + fi + + if [ -z "${secret_value}" ]; then + echo "ERROR: Secret '${secret_name}' from Key Vault '${vault_name}' returned an empty value." >&2 + return 1 + fi + + export "${variable_name}=${secret_value}" +} + +export_optional_secret() { + local variable_name="$1" + local vault_name="$2" + local secret_name="$3" + local secret_value + + if ! should_refresh_secrets && env_var_has_value "${variable_name}"; then + return 0 + fi + + if secret_value="$(kv_secret "${vault_name}" "${secret_name}" 2>/dev/null)"; then + export "${variable_name}=${secret_value}" + else + echo "WARN ${vault_name}/${secret_name}: not available; ${variable_name} not exported." >&2 + fi +} + +export_secret_aliases() { + local vault_name="$1" + local secret_name="$2" + shift 2 + local secret_value + local variable_name + + if ! should_refresh_secrets && all_env_vars_have_values "$@"; then + return 0 + fi + + if ! secret_value="$(kv_secret "${vault_name}" "${secret_name}")"; then + echo "ERROR: Failed to read '${secret_name}' from Key Vault '${vault_name}'." >&2 + return 1 + fi + + if [ -z "${secret_value}" ]; then + echo "ERROR: Secret '${secret_name}' from Key Vault '${vault_name}' returned an empty value." >&2 + return 1 + fi + + for variable_name in "$@"; do + export "${variable_name}=${secret_value}" + done +} + +export_secret_list() { + local secret_mode="$1" + local line + local variable_name + local vault_name + local secret_name + + while IFS='|' read -r variable_name vault_name secret_name; do + case "${variable_name}" in + ""|\#*) + continue + ;; + esac + + case "${secret_mode}" in + required) + export_secret "${variable_name}" "${vault_name}" "${secret_name}" || return 1 + ;; + optional) + export_optional_secret "${variable_name}" "${vault_name}" "${secret_name}" + ;; + *) + echo "ERROR: Unknown secret export mode '${secret_mode}'." >&2 + return 1 + ;; + esac + done +} + +clear_aat_connection_values() { + unset IDAM_API_URL_BASE IDAM_API_BASE_URL IDAM_URL IDAM_USER_URL IDAM_OIDC_URL OIDC_ISSUER + unset S2S_URL_BASE S2S_URL IDAM_S2S_URL + unset DEFINITION_STORE_HOST DEFINITION_STORE_URL_BASE + unset CASE_DOCUMENT_AM_URL USER_PROFILE_HOST + unset ROLE_ASSIGNMENT_URL ROLE_ASSIGNMENT_HOST + unset RD_LOCATION_REF_API_BASE_URL RD_PROFESSIONAL_API_BASE_URL + unset DM_STORE_BASE_URL BEFTA_TEST_STUB_SERVICE_BASE_URL + unset OAUTH2_CLIENT_ID CCD_API_GATEWAY_OAUTH2_CLIENT_ID + unset OAUTH2_REDIRECT_URI CCD_API_GATEWAY_OAUTH2_REDIRECT_URL + unset TEST_URL CCD_DATA_STORE_API_BASE_URL +} + +clear_aat_secret_values() { + unset OAUTH2_CLIENT_SECRET CCD_API_GATEWAY_OAUTH2_CLIENT_SECRET + unset CCD_API_GATEWAY_S2S_KEY BEFTA_S2S_CLIENT_SECRET CCD_GW_SERVICE_SECRET + unset ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_KEY BEFTA_S2S_CLIENT_SECRET_OF_CCD_DATA + unset BEFTA_S2S_CLIENT_SECRET_OF_XUI_WEBAPP BEFTA_S2S_CLIENT_SECRET_OF_AAC_MANAGE_CASE_ASSIGNMENT + unset DEFINITION_IMPORTER_USERNAME DEFINITION_IMPORTER_PASSWORD + unset CCD_CASEWORKER_AUTOTEST_EMAIL CCD_CASEWORKER_AUTOTEST_PASSWORD + unset CCD_IMPORT_AUTOTEST_EMAIL CCD_IMPORT_AUTOTEST_PASSWORD + unset ROLE_ASSIGNMENT_USER_EMAIL ROLE_ASSIGNMENT_USER_PASSWORD + unset CCD_PRIVATE_CASEWORKER_EMAIL CCD_PRIVATE_CASEWORKER_PASSWORD + unset CCD_PRIVATE_CASEWORKER_AUTOTEST_1AND2_PASSWORD + unset CCD_RESTRICTED_CASEWORKER_EMAIL CCD_RESTRICTED_CASEWORKER_PASSWORD + unset CCD_PRIVATE_CASEWORKER_SOLICITOR_EMAIL CCD_PRIVATE_CASEWORKER_SOLICITOR_PASSWORD + unset CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_EMAIL CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_PASSWORD + unset CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_EMAIL CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_PASSWORD + unset CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_EMAIL CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_PASSWORD + unset CCD_BEFTA_CASEWORKER_2_SOLICITOR_1_PWD CCD_BEFTA_CASEWORKER_2_SOLICITOR_2_PWD + unset CCD_BEFTA_CASEWORKER_2_SOLICITOR_3_PWD CCD_BEFTA_CASEWORKER_1_PWD + unset CCD_BEFTA_CASEWORKER_2_PWD CCD_BEFTA_CASEWORKER_3_PWD + unset CCD_BEFTA_CITIZEN_2_PWD CCD_BEFTA_CITIZEN_3_PWD + unset CCD_BEFTA_SOLICITOR_3_PWD CCD_BEFTA_CASEWORKER_1_NO_PROFILE_PWD + unset CCD_BEFTA_CASEWORKER_CAA_PWD CCD_BEFTA_MASTER_CASEWORKER_PWD + unset CCD_BEFTA_MASTER_SOLICITOR_1_PWD CCD_BEFTA_MASTER_SOLICITOR_2_PWD + unset CCD_BEFTA_MASTER_SOLICITOR_4_PWD +} + +smoke_secret_values_loaded() { + all_env_vars_have_values \ + OAUTH2_CLIENT_SECRET CCD_API_GATEWAY_OAUTH2_CLIENT_SECRET \ + CCD_API_GATEWAY_S2S_KEY BEFTA_S2S_CLIENT_SECRET CCD_GW_SERVICE_SECRET \ + ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_KEY BEFTA_S2S_CLIENT_SECRET_OF_CCD_DATA \ + DEFINITION_IMPORTER_USERNAME DEFINITION_IMPORTER_PASSWORD \ + CCD_CASEWORKER_AUTOTEST_EMAIL CCD_CASEWORKER_AUTOTEST_PASSWORD \ + CCD_IMPORT_AUTOTEST_EMAIL CCD_IMPORT_AUTOTEST_PASSWORD \ + ROLE_ASSIGNMENT_USER_EMAIL ROLE_ASSIGNMENT_USER_PASSWORD +} + +full_secret_values_loaded() { + smoke_secret_values_loaded && all_env_vars_have_values \ + BEFTA_S2S_CLIENT_SECRET_OF_XUI_WEBAPP BEFTA_S2S_CLIENT_SECRET_OF_AAC_MANAGE_CASE_ASSIGNMENT \ + CCD_PRIVATE_CASEWORKER_EMAIL CCD_PRIVATE_CASEWORKER_PASSWORD \ + CCD_PRIVATE_CASEWORKER_AUTOTEST_1AND2_PASSWORD \ + CCD_RESTRICTED_CASEWORKER_EMAIL CCD_RESTRICTED_CASEWORKER_PASSWORD \ + CCD_PRIVATE_CASEWORKER_SOLICITOR_EMAIL CCD_PRIVATE_CASEWORKER_SOLICITOR_PASSWORD \ + CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_EMAIL CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_PASSWORD \ + CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_EMAIL CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_PASSWORD \ + CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_EMAIL CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_PASSWORD \ + CCD_BEFTA_CASEWORKER_2_SOLICITOR_1_PWD CCD_BEFTA_CASEWORKER_2_SOLICITOR_2_PWD \ + CCD_BEFTA_CASEWORKER_2_SOLICITOR_3_PWD CCD_BEFTA_CASEWORKER_1_PWD \ + CCD_BEFTA_CASEWORKER_2_PWD CCD_BEFTA_CASEWORKER_3_PWD \ + CCD_BEFTA_CITIZEN_2_PWD CCD_BEFTA_CITIZEN_3_PWD \ + CCD_BEFTA_SOLICITOR_3_PWD CCD_BEFTA_CASEWORKER_1_NO_PROFILE_PWD \ + CCD_BEFTA_CASEWORKER_CAA_PWD CCD_BEFTA_MASTER_CASEWORKER_PWD \ + CCD_BEFTA_MASTER_SOLICITOR_1_PWD CCD_BEFTA_MASTER_SOLICITOR_2_PWD \ + CCD_BEFTA_MASTER_SOLICITOR_4_PWD +} + +needs_keyvault_access() { + local secret_profile="$1" + + should_refresh_secrets && return 0 + + case "${secret_profile}" in + smoke) + ! smoke_secret_values_loaded + ;; + full) + ! full_secret_values_loaded + ;; + *) + return 0 + ;; + esac +} + +export_remote_env_file() { + local env_name="${1:-aat}" + local env_file="${CCD_DATA_STORE_REMOTE_ENV_FILE:-.${env_name}-remote-env}" + local line + local key + local value + + if [ ! -f "${env_file}" ]; then + return 0 + fi + + while IFS= read -r line || [ -n "${line}" ]; do + case "${line}" in + ""|\#*|*=*) + ;; + *) + continue + ;; + esac + + case "${line}" in + ""|\#*) + continue + ;; + esac + + key="${line%%=*}" + value="${line#*=}" + + case "${key}" in + [A-Za-z_]*) + case "${key}" in + *[!A-Za-z0-9_]*) + continue + ;; + esac + ;; + *) + continue + ;; + esac + + export "${key}=${value}" + done < "${env_file}" + + export CCD_DATA_STORE_REMOTE_ENV_FILE_LOADED="${env_file}" +} + +export_remote_defaults() { + # IDAM: BEFTA /oauth2/authorize must use idam-api, not idam-web-public. + export IDAM_API_URL_BASE="${IDAM_API_URL_BASE:-${IDAM_API_BASE_URL:-https://idam-api.aat.platform.hmcts.net}}" + export IDAM_API_BASE_URL="${IDAM_API_BASE_URL:-${IDAM_API_URL_BASE}}" + export IDAM_URL="${IDAM_URL:-${IDAM_API_URL_BASE}}" + export IDAM_USER_URL="${IDAM_USER_URL:-https://idam-web-public.aat.platform.hmcts.net}" + export IDAM_OIDC_URL="${IDAM_OIDC_URL:-https://idam-web-public.aat.platform.hmcts.net}" + export OIDC_ISSUER="${OIDC_ISSUER:-${IDAM_OIDC_URL}/o}" + + export S2S_URL_BASE="${S2S_URL_BASE:-${IDAM_S2S_URL:-http://rpe-service-auth-provider-aat.service.core-compute-aat.internal}}" + export S2S_URL="${S2S_URL:-${S2S_URL_BASE}}" + export IDAM_S2S_URL="${IDAM_S2S_URL:-${S2S_URL_BASE}}" + + export DEFINITION_STORE_HOST="${DEFINITION_STORE_HOST:-http://ccd-definition-store-api-aat.service.core-compute-aat.internal}" + export DEFINITION_STORE_URL_BASE="${DEFINITION_STORE_URL_BASE:-${DEFINITION_STORE_HOST}}" + export CASE_DOCUMENT_AM_URL="${CASE_DOCUMENT_AM_URL:-http://ccd-case-document-am-api-aat.service.core-compute-aat.internal}" + export USER_PROFILE_HOST="${USER_PROFILE_HOST:-http://ccd-user-profile-api-aat.service.core-compute-aat.internal}" + export ROLE_ASSIGNMENT_URL="${ROLE_ASSIGNMENT_URL:-${ROLE_ASSIGNMENT_HOST:-http://am-role-assignment-service-aat.service.core-compute-aat.internal}}" + export ROLE_ASSIGNMENT_HOST="${ROLE_ASSIGNMENT_HOST:-${ROLE_ASSIGNMENT_URL}}" + export RD_LOCATION_REF_API_BASE_URL="${RD_LOCATION_REF_API_BASE_URL:-http://rd-location-ref-api-aat.service.core-compute-aat.internal}" + export RD_PROFESSIONAL_API_BASE_URL="${RD_PROFESSIONAL_API_BASE_URL:-http://rd-professional-api-aat.service.core-compute-aat.internal}" + export DM_STORE_BASE_URL="${DM_STORE_BASE_URL:-http://dm-store-aat.service.core-compute-aat.internal}" + export BEFTA_TEST_STUB_SERVICE_BASE_URL="${BEFTA_TEST_STUB_SERVICE_BASE_URL:-http://ccd-test-stubs-service-aat.service.core-compute-aat.internal}" +} + +apply_local_definition_store_override() { + if [ -n "${AAT_DEFINITION_STORE_HOST:-}" ]; then + export DEFINITION_STORE_HOST="${AAT_DEFINITION_STORE_HOST}" + export DEFINITION_STORE_URL_BASE="${AAT_DEFINITION_STORE_HOST}" + fi +} + +export_oauth_values() { + export OAUTH2_CLIENT_ID="${OAUTH2_CLIENT_ID:-ccd_gateway}" + export CCD_API_GATEWAY_OAUTH2_CLIENT_ID="${CCD_API_GATEWAY_OAUTH2_CLIENT_ID:-${OAUTH2_CLIENT_ID}}" + export OAUTH2_REDIRECT_URI="${OAUTH2_REDIRECT_URI:-https://www-ccd.nonprod.platform.hmcts.net/oauth2redirect}" + export CCD_API_GATEWAY_OAUTH2_REDIRECT_URL="${CCD_API_GATEWAY_OAUTH2_REDIRECT_URL:-${OAUTH2_REDIRECT_URI}}" + + export_secret_aliases ccd-aat ccd-api-gateway-oauth2-client-secret \ + OAUTH2_CLIENT_SECRET \ + CCD_API_GATEWAY_OAUTH2_CLIENT_SECRET +} + +export_s2s_values() { + local secret_profile="$1" + + export CCD_API_GATEWAY_S2S_ID="${CCD_API_GATEWAY_S2S_ID:-ccd_gw}" + export ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_ID="${ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_ID:-ccd_data}" + export_secret_aliases s2s-aat microservicekey-ccd-gw \ + CCD_API_GATEWAY_S2S_KEY \ + BEFTA_S2S_CLIENT_SECRET \ + CCD_GW_SERVICE_SECRET \ + || return 1 + export_secret_aliases s2s-aat microservicekey-ccd-data \ + ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_KEY \ + BEFTA_S2S_CLIENT_SECRET_OF_CCD_DATA \ + || return 1 + + export CCD_GW_SERVICE_NAME="${CCD_GW_SERVICE_NAME:-${CCD_API_GATEWAY_S2S_ID}}" + export BEFTA_S2S_CLIENT_ID="${BEFTA_S2S_CLIENT_ID:-${CCD_API_GATEWAY_S2S_ID}}" + export BEFTA_S2S_CLIENT_ID_OF_CCD_DATA="${BEFTA_S2S_CLIENT_ID_OF_CCD_DATA:-ccd_data}" + export BEFTA_S2S_CLIENT_ID_OF_XUI_WEBAPP="${BEFTA_S2S_CLIENT_ID_OF_XUI_WEBAPP:-xui_webapp}" + + if [ "${secret_profile}" = "full" ]; then + export_optional_secret BEFTA_S2S_CLIENT_SECRET_OF_XUI_WEBAPP s2s-aat microservicekey-xui-webapp + export_optional_secret BEFTA_S2S_CLIENT_SECRET_OF_AAC_MANAGE_CASE_ASSIGNMENT \ + s2s-aat microservicekey-aac-manage-case-assignment + fi +} + +export_required_user_secrets() { + export_secret_list required <<'EOF' +DEFINITION_IMPORTER_USERNAME|ccd-aat|definition-importer-username +DEFINITION_IMPORTER_PASSWORD|ccd-aat|definition-importer-password +CCD_CASEWORKER_AUTOTEST_EMAIL|ccd-aat|ccd-caseworker-autotest-email +CCD_CASEWORKER_AUTOTEST_PASSWORD|ccd-aat|ccd-caseworker-autotest-password +CCD_IMPORT_AUTOTEST_EMAIL|ccd-aat|ccd-importer-autotest-email +CCD_IMPORT_AUTOTEST_PASSWORD|ccd-aat|ccd-importer-autotest-password +ROLE_ASSIGNMENT_USER_EMAIL|ccd-aat|idam-data-store-system-user-username +ROLE_ASSIGNMENT_USER_PASSWORD|ccd-aat|idam-data-store-system-user-password +EOF +} + +export_full_functional_secrets() { + export_secret_list optional <<'EOF' +CCD_PRIVATE_CASEWORKER_EMAIL|ccd-aat|ccd-private-caseworker-email +CCD_PRIVATE_CASEWORKER_PASSWORD|ccd-aat|ccd-private-caseworker-password +CCD_PRIVATE_CASEWORKER_AUTOTEST_1AND2_PASSWORD|ccd-aat|ccd-private-caseworker-autotest-1and2-password +CCD_RESTRICTED_CASEWORKER_EMAIL|ccd-aat|ccd-restricted-caseworker-email +CCD_RESTRICTED_CASEWORKER_PASSWORD|ccd-aat|ccd-restricted-caseworker-password +CCD_PRIVATE_CASEWORKER_SOLICITOR_EMAIL|ccd-aat|ccd-private-caseworker-solicitor-email +CCD_PRIVATE_CASEWORKER_SOLICITOR_PASSWORD|ccd-aat|ccd-private-caseworker-solicitor-password +CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_EMAIL|ccd-aat|ccd-private-cross-case-type-worker-email +CCD_PRIVATE_CROSS_CASE_TYPE_CASEWORKER_PASSWORD|ccd-aat|ccd-private-cross-case-type-caseworker-password +CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_EMAIL|ccd-aat|ccd-private-cross-case-type-solicitor-email +CCD_PRIVATE_CROSS_CASE_TYPE_SOLICITOR_PASSWORD|ccd-aat|ccd-private-cross-case-type-solicitor-password +CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_EMAIL|ccd-aat|ccd-restricted-cross-case-type-caseworker-email +CCD_RESTRICTED_CROSS_CASE_TYPE_CASEWORKER_PASSWORD|ccd-aat|ccd-restricted-cross-case-type-caseworker-password +CCD_BEFTA_CASEWORKER_2_SOLICITOR_1_PWD|ccd-aat|ccd-befta-caseworker-2-solicitor-1-pwd +CCD_BEFTA_CASEWORKER_2_SOLICITOR_2_PWD|ccd-aat|ccd-befta-caseworker-2-solicitor-2-pwd +CCD_BEFTA_CASEWORKER_2_SOLICITOR_3_PWD|ccd-aat|ccd-befta-caseworker-2-solicitor-3-pwd +CCD_BEFTA_CASEWORKER_1_PWD|ccd-aat|ccd-befta-caseworker-1-pwd +CCD_BEFTA_CASEWORKER_2_PWD|ccd-aat|ccd-befta-caseworker-2-pwd +CCD_BEFTA_CASEWORKER_3_PWD|ccd-aat|ccd-befta-caseworker-3-pwd +CCD_BEFTA_CITIZEN_2_PWD|ccd-aat|ccd-befta-citizen-2-pwd +CCD_BEFTA_CITIZEN_3_PWD|ccd-aat|ccd-befta-citizen-3-pwd +CCD_BEFTA_SOLICITOR_3_PWD|ccd-aat|ccd-befta-solicitor-3-pwd +CCD_BEFTA_CASEWORKER_1_NO_PROFILE_PWD|ccd-aat|ccd-befta-caseworker-1-no-profile-pwd +CCD_BEFTA_CASEWORKER_CAA_PWD|ccd-aat|ccd-befta-caseworker-caa-pwd +CCD_BEFTA_MASTER_CASEWORKER_PWD|ccd-aat|ccd-befta-master-caseworker-pwd +CCD_BEFTA_MASTER_SOLICITOR_1_PWD|ccd-aat|ccd-befta-master-solicitor1-pwd +CCD_BEFTA_MASTER_SOLICITOR_2_PWD|ccd-aat|ccd-befta-master-solicitor2-pwd +CCD_BEFTA_MASTER_SOLICITOR_4_PWD|ccd-aat|ccd-befta-master-solicitor4-pwd +EOF +} + +export_test_target() { + case "${CCD_DATA_STORE_TARGET:-local}" in + local) + export TEST_URL="${TEST_URL:-http://localhost:4452}" + ;; + remote|staging|aat) + export TEST_URL="${TEST_URL:-http://ccd-data-store-api-aat.service.core-compute-aat.internal}" + ;; + *) + echo "Unknown CCD_DATA_STORE_TARGET='${CCD_DATA_STORE_TARGET}'. Use 'local' or 'remote'." >&2 + return 1 + ;; + esac + + export CCD_DATA_STORE_API_BASE_URL="${TEST_URL}" +} + +export_befta_knobs() { + export BEFTA_RESPONSE_HEADER_CHECK_POLICY="${BEFTA_RESPONSE_HEADER_CHECK_POLICY:-JUST_WARN}" + export BEFTA_RETRY_MAX_ATTEMPTS="${BEFTA_RETRY_MAX_ATTEMPTS:-3}" + export BEFTA_RETRY_STATUS_CODES="${BEFTA_RETRY_STATUS_CODES:-500,502,503,504}" + export BEFTA_RETRY_MAX_DELAY="${BEFTA_RETRY_MAX_DELAY:-1000}" + export BEFTA_RETRY_NON_RETRYABLE_HTTP_METHODS="${BEFTA_RETRY_NON_RETRYABLE_HTTP_METHODS:-POST,PUT}" + export DEFAULT_COLLECTION_ASSERTION_MODE="${DEFAULT_COLLECTION_ASSERTION_MODE:-UNORDERED}" + export GROUP_ACCESS_ENABLED="${GROUP_ACCESS_ENABLED:-true}" + export ENABLE_CASE_GROUP_ACCESS_FILTERING="${ENABLE_CASE_GROUP_ACCESS_FILTERING:-${GROUP_ACCESS_ENABLED}}" +} + +print_aat_env_summary() { + local secret_profile="$1" + + echo "AAT environment exported:" + echo " AAT_ENV_MODE=${secret_profile}" + echo " IDAM_API_URL_BASE=${IDAM_API_URL_BASE}" + echo " S2S_URL_BASE=${S2S_URL_BASE}" + echo " OAUTH2_CLIENT_ID=${OAUTH2_CLIENT_ID}" + echo " OAUTH2_REDIRECT_URI=${OAUTH2_REDIRECT_URI}" + echo " DEFINITION_STORE_URL_BASE=${DEFINITION_STORE_URL_BASE}" + echo " TEST_URL=${TEST_URL}" + echo " GROUP_ACCESS_ENABLED=${GROUP_ACCESS_ENABLED}" + echo " ENABLE_CASE_GROUP_ACCESS_FILTERING=${ENABLE_CASE_GROUP_ACCESS_FILTERING}" +} + +aat_env_main() { + local secret_profile + + case "${1:-}" in + help|-h|--help) + print_aat_env_help + return 0 + ;; + esac + + secret_profile="$(aat_env_secret_profile "$1")" || return 1 + + if should_refresh_secrets; then + clear_aat_secret_values + fi + + if needs_keyvault_access "${secret_profile}"; then + require_az_cli || return 1 + require_azure_keyvault_token || return 1 + fi + + clear_aat_connection_values + export_remote_env_file aat || return 1 + export_remote_defaults + apply_local_definition_store_override + export_oauth_values || return 1 + export_s2s_values "${secret_profile}" || return 1 + export_required_user_secrets || return 1 + + if [ "${secret_profile}" = "full" ]; then + export_full_functional_secrets || return 1 + fi + + export_test_target || return 1 + export_befta_knobs + print_aat_env_summary "${secret_profile}" +} + +aat_env_main "$@" || return 1 2>/dev/null || exit 1 diff --git a/scripts/check-remote-env.sh b/scripts/check-remote-env.sh new file mode 100755 index 0000000000..275362dd3c --- /dev/null +++ b/scripts/check-remote-env.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +set -u + +env_name="${1:-aat}" +env_file=".${env_name}-remote-env" + +case "${env_name}" in + aat|AAT) run_task="runRemoteAAT" ;; + demo|Demo) run_task="runRemoteDemo" ;; + *) run_task="runRemote$(printf '%s' "${env_name}" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')" ;; +esac + +if [ ! -f "${env_file}" ]; then + echo "ERROR: ${env_file} does not exist." >&2 + echo "Create it first: ./gradlew reloadEnvSecrets -Penv=${env_name}" >&2 + exit 1 +fi + +load_env_file() { + while IFS= read -r line || [ -n "${line}" ]; do + case "${line}" in + ''|\#*) continue ;; + esac + + key="${line%%=*}" + value="${line#*=}" + + if [ "${key}" = "${line}" ]; then + echo "WARN ${env_file}: ignoring line without '='." >&2 + continue + fi + + case "${key}" in + ''|*[!A-Za-z0-9_]*) + echo "WARN ${env_file}: ignoring invalid key '${key}'." >&2 + continue + ;; + esac + + export "${key}=${value}" + done < "${env_file}" +} + +apply_alias() { + local alias="$1" + local source="$2" + eval "source_value=\${${source}:-}" + + if [ -n "${source_value}" ]; then + export "${alias}=${source_value}" + fi +} + +unset DATA_STORE_DB_HOST DATA_STORE_DB_PORT +unset IDAM_S2S_URL S2S_URL_BASE +unset IDAM_API_BASE_URL IDAM_API_URL_BASE +unset IDAM_OIDC_URL OIDC_ISSUER +unset DEFINITION_STORE_HOST DEFINITION_STORE_URL_BASE +unset USER_PROFILE_HOST CASE_DOCUMENT_AM_URL +unset ROLE_ASSIGNMENT_URL ROLE_ASSIGNMENT_HOST +unset RD_LOCATION_REF_API_BASE_URL +unset ELASTIC_SEARCH_HOSTS ELASTIC_SEARCH_DATA_NODES_HOSTS ELASTIC_SEARCH_DATA_NODES_URL + +load_env_file +apply_alias "IDAM_API_URL_BASE" "IDAM_API_BASE_URL" +apply_alias "S2S_URL_BASE" "IDAM_S2S_URL" +apply_alias "DEFINITION_STORE_URL_BASE" "DEFINITION_STORE_HOST" +apply_alias "ROLE_ASSIGNMENT_HOST" "ROLE_ASSIGNMENT_URL" +apply_alias "TEST_URL" "CCD_DATA_STORE_API_BASE_URL" + +if [ -n "${AAT_DEFINITION_STORE_HOST:-}" ]; then + export DEFINITION_STORE_HOST="${AAT_DEFINITION_STORE_HOST}" + export DEFINITION_STORE_URL_BASE="${AAT_DEFINITION_STORE_HOST}" +fi + +status=0 + +is_aat_env() { + case "${env_name}" in + aat|AAT) + return 0 + ;; + *) + return 1 + ;; + esac +} + +clean_url() { + printf '%s' "$1" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//; s/^"//; s/"$//' +} + +host_from_url() { + clean_url "$1" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#/.*$##; s#:.*$##' +} + +port_from_url() { + local url="$1" + local default_port="$2" + local host_port + host_port="$(clean_url "${url}" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#/.*$##')" + case "${host_port}" in + *:*) printf '%s' "${host_port##*:}" ;; + *) printf '%s' "${default_port}" ;; + esac +} + +can_resolve() { + local host="$1" + + if command -v dscacheutil >/dev/null 2>&1 && dscacheutil -q host -a name "${host}" >/dev/null 2>&1; then + return 0 + fi + if command -v getent >/dev/null 2>&1 && getent hosts "${host}" >/dev/null 2>&1; then + return 0 + fi + if command -v nslookup >/dev/null 2>&1 && nslookup "${host}" >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + +print_dns_hint() { + local host="$1" + + case "${host}" in + host.docker.internal) + echo " host.docker.internal is only for a process running inside Docker that must call a service on the host machine." >&2 + ;; + *.service.core-compute-aat.internal|*.internal) + echo " ${host} is an AAT internal hostname. Connect to the required HMCTS VPN/bastion/private DNS route, or override/tunnel this dependency before running ${run_task}." >&2 + ;; + esac +} + +check_aat_s2s_url() { + local url="$1" + local host + + [ -n "${url}" ] || return + is_aat_env || return + + host="$(host_from_url "${url}")" + case "${host}" in + rpe-service-auth-provider-aat.service.core-compute-aat.internal|localhost|127.0.0.1) + return 0 + ;; + host.docker.internal) + echo "FAIL s2s: AAT IDAM_S2S_URL/S2S_URL_BASE uses ${url}" >&2 + echo " For normal local Gradle ${run_task}, use http://rpe-service-auth-provider-aat.service.core-compute-aat.internal." >&2 + echo " If using a host S2S tunnel from a Mac app process, use http://localhost:." >&2 + echo " Use host.docker.internal only for a process running inside Docker that calls a host-machine tunnel." >&2 + ;; + *) + echo "FAIL s2s: AAT IDAM_S2S_URL/S2S_URL_BASE uses unexpected host '${host}' from ${url}" >&2 + echo " Expected http://rpe-service-auth-provider-aat.service.core-compute-aat.internal, or http://localhost: for an intentional host S2S tunnel." >&2 + ;; + esac + + status=1 + return 1 +} + +check_aat_elastic_url() { + local url="$1" + local host + + [ -n "${url}" ] || return + is_aat_env || return + + host="$(host_from_url "${url}")" + case "${host}" in + ccd-data-*.service.core-compute-aat.internal|localhost|127.0.0.1) + return 0 + ;; + host.docker.internal) + echo "FAIL elastic search: AAT ELASTIC_SEARCH_HOSTS/ELASTIC_SEARCH_DATA_NODES_HOSTS uses ${url}" >&2 + echo " For normal local Gradle ${run_task}, use http://ccd-data-.service.core-compute-aat.internal:9200." >&2 + echo " If using a host Elasticsearch tunnel from a Mac app process, use http://localhost:." >&2 + echo " Use host.docker.internal only for a process running inside Docker that calls a host-machine tunnel." >&2 + ;; + *) + echo "FAIL elastic search: AAT ELASTIC_SEARCH_HOSTS/ELASTIC_SEARCH_DATA_NODES_HOSTS uses unexpected host '${host}' from ${url}" >&2 + echo " Expected http://ccd-data-.service.core-compute-aat.internal:9200, or http://localhost: for an intentional host Elasticsearch tunnel." >&2 + ;; + esac + + status=1 + return 1 +} + +check_tcp() { + local label="$1" + local host="$2" + local port="$3" + + if [ -z "${host}" ] || [ -z "${port}" ]; then + echo "FAIL ${label}: missing host or port" >&2 + status=1 + return + fi + + if ! can_resolve "${host}"; then + echo "FAIL ${label}: cannot resolve ${host}" >&2 + print_dns_hint "${host}" + status=1 + return + fi + + if nc -G 5 -z "${host}" "${port}" >/dev/null 2>&1 || nc -w 5 -z "${host}" "${port}" >/dev/null 2>&1; then + echo "OK ${label}: ${host}:${port}" + else + echo "FAIL ${label}: cannot connect to ${host}:${port}" >&2 + status=1 + fi +} + +check_url() { + local label="$1" + local url="$2" + local default_port="$3" + + [ -n "${url}" ] || return + check_tcp "${label}" "$(host_from_url "${url}")" "$(port_from_url "${url}" "${default_port}")" +} + +check_url_list() { + local label="$1" + local urls="$2" + local default_port="$3" + local index=0 + local url + + [ -n "${urls}" ] || return + + while IFS= read -r url; do + url="$(clean_url "${url}")" + [ -n "${url}" ] || continue + check_url "${label}[${index}]" "${url}" "${default_port}" + index=$((index + 1)) + done < <(printf '%s' "${urls}" | tr ',' '\n') +} + +check_elastic_url_list() { + local label="$1" + local urls="$2" + local default_port="$3" + local index=0 + local url + + [ -n "${urls}" ] || return + + while IFS= read -r url; do + url="$(clean_url "${url}")" + [ -n "${url}" ] || continue + if check_aat_elastic_url "${url}"; then + check_url "${label}[${index}]" "${url}" "${default_port}" + fi + index=$((index + 1)) + done < <(printf '%s' "${urls}" | tr ',' '\n') +} + +check_health() { + local label="$1" + local url="$2" + local health_url + + [ -n "${url}" ] || return + + if ! command -v curl >/dev/null 2>&1; then + echo "WARN ${label}: curl not available; skipping health check" + return + fi + + health_url="${url%/}/health" + if curl --fail --silent --show-error --max-time 10 "${health_url}" >/dev/null; then + echo "OK ${label} health: ${health_url}" + else + echo "FAIL ${label} health: ${health_url}" >&2 + status=1 + fi +} + +s2s_url="${IDAM_S2S_URL:-${S2S_URL_BASE:-}}" +check_tcp "database" "${DATA_STORE_DB_HOST:-}" "${DATA_STORE_DB_PORT:-5432}" +if check_aat_s2s_url "${s2s_url}"; then + check_url "s2s" "${s2s_url}" 80 +fi +check_url "idam api" "${IDAM_API_BASE_URL:-${IDAM_API_URL_BASE:-}}" 443 +check_url "idam oidc" "${IDAM_OIDC_URL:-${OIDC_ISSUER:-}}" 443 +check_url "definition store" "${DEFINITION_STORE_HOST:-}" 80 +if [ -n "${AAT_DEFINITION_STORE_HOST:-}" ]; then + check_health "definition store" "${DEFINITION_STORE_HOST:-}" +fi +check_url "user profile" "${USER_PROFILE_HOST:-}" 80 +check_url "case document" "${CASE_DOCUMENT_AM_URL:-}" 80 +check_url "role assignment" "${ROLE_ASSIGNMENT_URL:-${ROLE_ASSIGNMENT_HOST:-}}" 80 +check_url "location reference" "${RD_LOCATION_REF_API_BASE_URL:-}" 80 +check_elastic_url_list "elastic search" "${ELASTIC_SEARCH_HOSTS:-${ELASTIC_SEARCH_DATA_NODES_HOSTS:-${ELASTIC_SEARCH_DATA_NODES_URL:-}}}" 9200 + +if [ "${status}" -ne 0 ]; then + echo + echo "One or more remote dependencies are unreachable. Check VPN/bastion/DNS before running ${run_task}." >&2 +fi + +exit "${status}"