Skip to content

Commit

Permalink
Merge pull request #16768 from CDCgov/deployment/2024-12-10
Browse files Browse the repository at this point in the history
Deployment of 2024-12-10
  • Loading branch information
adegolier authored Dec 10, 2024
2 parents 66927a2 + 3d782a3 commit e6a4645
Show file tree
Hide file tree
Showing 67 changed files with 2,493 additions and 883 deletions.
94 changes: 94 additions & 0 deletions .github/actions/checksum-validate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Checksum Validate Action

[![Test Action](https://github.com/JosiahSiegel/checksum-validate-action/actions/workflows/test_action.yml/badge.svg)](https://github.com/JosiahSiegel/checksum-validate-action/actions/workflows/test_action.yml)

## Synopsis

1. Generate a checksum from either a string or shell command (use command substitution: `$()`).
2. Validate if checksum is identical to input (even across multiple jobs), using a `key` to link the validation attempt with the correct generated checksum.
* Validation is possible across jobs since the checksum is uploaded as a workflow artifact

## Usage

```yml
jobs:
generate-checksums:
name: Generate checksum
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]

- name: Generate checksum of string
uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236
with:
key: test string
input: hello world

- name: Generate checksum of command output
uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236
with:
key: test command
input: $(cat action.yml)

validate-checksums:
name: Validate checksum
needs:
- generate-checksums
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]

- name: Validate checksum of valid string
id: valid-string
uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236
with:
key: test string
validate: true
fail-invalid: true
input: hello world

- name: Validate checksum of valid command output
id: valid-command
uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236
with:
key: test command
validate: true
fail-invalid: true
input: $(cat action.yml)

- name: Get outputs
run: |
echo ${{ steps.valid-string.outputs.valid }}
echo ${{ steps.valid-command.outputs.valid }}
```
## Workflow summary
### ✅ test string checksum valid ✅
### ❌ test string checksum INVALID ❌
## Inputs
```yml
inputs:
validate:
description: Check if checksums match
default: false
key:
description: String to keep unique checksums separate
required: true
fail-invalid:
description: Fail step if invalid checksum
default: false
input:
description: String or command for checksum
required: true
```
## Outputs
```yml
outputs:
valid:
description: True if checksums match
```
111 changes: 111 additions & 0 deletions .github/actions/checksum-validate/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# action.yml
name: Checksum Validate Action
description: Generate and validate checksums
branding:
icon: 'lock'
color: 'orange'
inputs:
validate:
description: Check if checksums match
default: false
key:
description: String to keep unique checksums separate
required: true
fail-invalid:
description: Fail step if invalid checksum
default: false
input:
description: String or command for checksum
required: true
outputs:
valid:
description: True if checksums match
value: ${{ steps.validate_checksum.outputs.valid }}

runs:
using: "composite"
steps:

# CHECKSUM START
- name: Generate SHA
uses: nick-fields/[email protected]
with:
max_attempts: 5
retry_on: any
timeout_seconds: 10
retry_wait_seconds: 15
command: |
function fail {
printf '%s\n' "$1" >&2
exit "${2-1}"
}
input_cmd="${{ inputs.input }}" || fail
sha="$(echo "$input_cmd" | sha256sum)"
echo "sha=$sha" >> $GITHUB_ENV
echo "success=true" >> $GITHUB_ENV
- name: Get input SHA
if: env.success
id: input_sha
shell: bash
run: echo "sha=${{ env.sha }}" >> $GITHUB_OUTPUT

- name: Get input SHA
if: env.success != 'true'
shell: bash
run: |
echo "failed to generate sha"
exit 1
# CHECKSUM END

# UPLOAD FILE START
- name: Create checksum file
if: inputs.validate != 'true'
shell: bash
run: |
echo "${{ steps.input_sha.outputs.sha }}" > "${{ github.sha }}-${{ inputs.key }}.txt"
- name: Upload checksum file
if: inputs.validate != 'true'
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3
with:
name: "${{ github.sha }}-${{ inputs.key }}.txt"
path: "${{ github.sha }}-${{ inputs.key }}.txt"
retention-days: 5
# UPLOAD FILE END

# VALIDATE FILE START
- name: Download checksum file
if: inputs.validate == 'true'
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe
with:
name: "${{ github.sha }}-${{ inputs.key }}.txt"

- name: Validate pre and post checksums
if: inputs.validate == 'true'
id: validate_checksum
shell: bash
run: |
echo "${{ steps.input_sha.outputs.sha }}" > "${{ github.sha }}-${{ inputs.key }}-2.txt"
DIFF=$(diff -q "${{ github.sha }}-${{ inputs.key }}-2.txt" "${{ github.sha }}-${{ inputs.key }}.txt") || true
codevalid=true
if [ "$DIFF" != "" ]
then
codevalid=false
fi
echo "valid=$codevalid" >> $GITHUB_OUTPUT
- name: Create summary
if: inputs.validate == 'true'
run: |
# Use ternary operator to assign emoji based on validity
emoji=${{ steps.validate_checksum.outputs.valid == 'true' && '✅' || '❌' }}
valid=${{ steps.validate_checksum.outputs.valid == 'true' && 'valid' || 'INVALID' }}
echo "### $emoji ${{ inputs.key }} checksum $valid $emoji" >> $GITHUB_STEP_SUMMARY
shell: bash
# VALIDATE FILE END

- name: Fail if invalid checksum
if: inputs.validate == 'true' && steps.validate_checksum.outputs.valid == 'false' && inputs.fail-invalid == 'true'
run: exit 1
shell: bash
5 changes: 1 addition & 4 deletions .github/actions/deploy-backend/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,7 @@ runs:

- name: Validate function app checksum
if: inputs.checksum-validation == 'true'

uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236
## DevSecOps - Aquia (Replace) uses: ./.github/actions/checksum-validate-action

uses: ./.github/actions/checksum-validate
with:
key: backend
validate: true
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ updates:
schedule:
interval: "daily"

- package-ecosystem: "github-actions"
directory: "/.github/actions/checksum-validate"
schedule:
interval: "daily"

# Frontend
- package-ecosystem: "npm"
directory: "/frontend-react"
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/release_to_azure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,7 @@ jobs:
env:
checksum_validation: ${{ vars.CHECKSUM_VALIDATION }}
if: needs.pre_job.outputs.has_router_change == 'true' && env.checksum_validation == 'true'

uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236
## DevSecOps - Aquia (Replace) - uses: ./.github/actions/checksum-validate-action

uses: ./.github/actions/checksum-validate
with:
key: backend
input: $(az functionapp config appsettings list -g prime-data-hub-${{ needs.pre_job.outputs.env_name }} -n pdh${{ needs.pre_job.outputs.env_name }}-functionapp -o tsv | sort)
Expand Down
14 changes: 7 additions & 7 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apply(from = rootProject.file("buildSrc/shared.gradle.kts"))

plugins {
id("org.springframework.boot") version "3.3.5"
id("org.springframework.boot") version "3.4.0"
id("io.spring.dependency-management") version "1.1.6"
id("reportstream.project-conventions")
kotlin("plugin.spring") version "2.0.21"
Expand All @@ -26,16 +26,16 @@ dependencies {

runtimeOnly("com.nimbusds:oauth2-oidc-sdk:11.20.1")

// okta
implementation("com.okta.sdk:okta-sdk-api:20.0.0")
runtimeOnly("com.okta.sdk:okta-sdk-impl:20.0.0")

// Swagger
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")

testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")

compileOnly("org.springframework.boot:spring-boot-devtools")
}
Expand All @@ -49,7 +49,7 @@ configurations.all {
dependencyManagement {
imports {
mavenBom("com.azure.spring:spring-cloud-azure-dependencies:5.18.0")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0")
}
}

Expand Down
56 changes: 56 additions & 0 deletions auth/docs/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Running the Auth Microservice

## Prerequisites

A few secrets are required to run the Auth which are not committed to source. These values are
configured in Okta.

| Environment variable | Value |
|----------------------|---------------------------------|
| OKTA_ADMIN_CLIENT_API_ENCODED_PRIVATE_KEY | Base 64 encoded private key pem |
| SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT_SECRET | Base 64 encoded secret |

## How to run application locally

```bash
# from project root
# start ReportStream and all dependent docker containers
./gradlew quickRun
# start submissions service
./ gradlew submissions:bootRun
# start auth service
./gradlew auth:bootRun
```

## Setup a Sender

- Sign in to Admin Okta
- Applications -> Application tab
- Click "Create App Integration"
- Select "API Services" and click next
- Name your sender
- Copy your client ID and client secret or private key locally to be used while calling the /token endpoint
- Add the user to the appropriate sender group
- You can find this option on the small gear next to your newly created application
- Ensure the group has the prefix DHSender_

## Submitting reports locally

- Retrieve an access token directly from Okta and ensure the JWT contains the "sender" scope
- Make a well-formed request to https://reportstream.oktapreview.com/oauth2/default/v1/token to retrieve your access token
- [See Okta documentation on that endpoint here](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/#get-an-access-token)
- Submit your report to http://localhost:9000/api/v1/reports
- Note the it's port 9000 which is auth rather than directly to 8880 which is submissions
- See endpoint definition in [SubmissionController](../../submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt)
- Add the access token you retrieved from Okta as a `Bearer` token in the `Authorization` header
- Inspect the logs if you received a 401 or a 403. This indicates there is an issue with your access token.

## Notes on secrets

The Okta-Groups JWT signing key pair has a local dev value already set up appropriately in auth and
downstream in submissions. New values _must_ be generated for deployed environments. You can look
at [KeyGenerationUtils](../src/test/kotlin/gov/cdc/prime/reportstream/auth/util/KeyGenerationUtils.kt)
for scripts to run to generate new keys.

By Default, we are connecting to the Staging Okta. We cannot post connection secrets directly in this document so
you will have to ask someone for those values.
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ object AuthApplicationConstants {
const val HEALTHCHECK_ENDPOINT_V1 = "/api/v1/healthcheck"
}

/**
* All Submissions service endpoints defined here
*/
object SubmissionsEndpoints {
const val REPORTS_ENDPOINT_V1 = "/api/v1/reports"
object Scopes {
const val ORGANIZATION_SCOPE = "organization"
const val SUBJECT_SCOPE = "sub"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gov.cdc.prime.reportstream.auth.client

import com.okta.sdk.resource.api.ApplicationGroupsApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.logging.log4j.kotlin.Logging
import org.springframework.stereotype.Service

@Service
class OktaGroupsClient(
private val applicationGroupsApi: ApplicationGroupsApi,
) : Logging {

/**
* Get all application groups from the Okta Admin API
*
* Group names are found at json path "_embedded.group.profile.name"
*
* @see https://developer.okta.com/docs/api/openapi/okta-management/management/tag/ApplicationGroups/#tag/ApplicationGroups/operation/listApplicationGroupAssignments
*/
suspend fun getApplicationGroups(appId: String): List<String> {
return withContext(Dispatchers.IO) {
try {
val groups = applicationGroupsApi
.listApplicationGroupAssignments(appId, null, null, null, "group")
.map { it.embedded?.get("group") as Map<*, *> }
.map { it["profile"] as Map<*, *> }
.map { it["name"] as String }
logger.info("$appId is a member of ${groups.joinToString()}")
groups
} catch (ex: Exception) {
logger.error("Error retrieving application groups from Okta API", ex)
throw ex
}
}
}
}
Loading

0 comments on commit e6a4645

Please sign in to comment.