Skip to content

Commit

Permalink
Merge branch 'main' into mt-add-libcurl
Browse files Browse the repository at this point in the history
  • Loading branch information
mojotalantikite committed Feb 8, 2024
2 parents 56dae2f + d241965 commit 68e2515
Show file tree
Hide file tree
Showing 97 changed files with 2,158 additions and 1,031 deletions.
3 changes: 2 additions & 1 deletion .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export CT_URL='https://cloudtamer.cms.gov/'
export CT_AWS_ROLE='ct-ado-managedcare-developer-admin'
export CT_IDMS='2'

# values formerly in .env (required)
# required values
export SASS_PATH='src:../../node_modules'
export REACT_APP_AUTH_MODE='LOCAL'
export REACT_APP_STAGE_NAME='local'
Expand All @@ -26,6 +26,7 @@ export DATABASE_URL='postgresql://postgres:shhhsecret@localhost:5432/postgres?sc
export EMAILER_MODE='LOCAL'
export LD_SDK_KEY='this-value-must-be-set-in-local'
export PARAMETER_STORE_MODE='LOCAL'
export JWT_SECRET='3fd2e448ed2cec1fa46520f1b64bcb243c784f68db41ea67ef9abc45c12951d3e770162829103c439f01d2b860d06ed0da1a08895117b1ef338f1e4ed176448a' # pragma: allowlist secret

export REACT_APP_OTEL_COLLECTOR_URL='http://localhost:4318/v1/traces'
export REACT_APP_LD_CLIENT_ID='this-value-can-be-set-in-local-if-desired'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ jobs:
working-directory: services/uploads/src/avLayer
run: ./dockerbuild.sh

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: lambda-layers-clamav
path: ./services/uploads/src/avLayer/build/lambda_layer.zip
Expand Down
122 changes: 80 additions & 42 deletions docs/technical-design/launch-darkly-testing-approach.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,60 +23,98 @@ export LD_SDK_KEY='Place Launch Darkly SDK key here'
## Feature flag unit testing

### Client side unit testing
Client side unit testing utilizes `jest.spyOn()` to mock the LaunchDarkly `useLDClient` hook and return default flag values or values specified. This implementation is done in our jest helper function `ldUseClientSpy()` located in `app-web/src/testHelpers/jestHelpers.tsx`.
Client side unit testing utilizes the `LDProvider`, from `launchdarkly-react-client-sdk`, in our [renderWithProviders](../../services/app-web/src/testHelpers/jestHelpers.tsx) function to set up feature flags for each of the tests. Currently, LaunchDarkly does not provide an actual mock `LDProvider` so if or when they do, then we could update this with that provider.

`ldUseClientSpy` takes in an object of feature flags and values as an argument. You can configure multiple flags with the object passed into `ldUseClientSpy`.
We use this method for testing because the official documented [unit testing](https://docs.launchdarkly.com/guides/sdk/unit-tests/?q=unit+test) method by LaunchDarkly does not work with our LaunchDarkly implementation. Our implementation follow exactly the documentation, so it could be how we are setting up our unit tests. Previously we had used `jest.spyOn` to intercept `useLDClient` and mock the `useLDClient.variation()` function with our defined feature flag values. With `launchdarkly-react-client-sdk@3.0.10` that method did not work anymore.

```javascript
ldUseClientSpy({
'rates-across-submissions': true,
'rate-cert-assurance': true,
})
```
#### Configuration
When using the `LDProvider` we need to pass in a mocked `ldClient` in the configuration. This allows us to initialize `ldClient` outside of the provider, which would have required the provider to perform an API call to LaunchDarkly. Now that this API call does not happen it isolates our unit tests from the feature flag values on the LaunchDarkly server and only use the values we define in each test.

To configure feature flags for a single test place `ldUseClientSpy` at the beginning of your test.
The configuration below, in `renderWithProviders`, the `ldClient` field is how we initialize `ldClient` with our defined flag values. We are using the `ldClientMock()` function to generate a mock that matches the type this field requires.

```javascript
it('cannot continue if no documents are added to the second rate', async () => {
ldUseClientSpy({ 'rates-across-submissions': true })
const mockUpdateDraftFn = jest.fn()
renderWithProviders(
<RateDetails
draftSubmission={emptyRateDetailsDraft}
updateDraft={mockUpdateDraftFn}
previousDocuments={[]}
/>,
{
apolloProvider: {
mocks: [fetchCurrentUserMock({ statusCode: 200 })],
},
}
)
You will also see that, compared to our configuration in [app-web/src/index.tsx](../../services/app-web/src/index.tsx), the config needed to connect to LaunchDarkly is replaced with `test-url`.

...
})
```typescript
const ldProviderConfig: ProviderConfig = {
clientSideID: 'test-url',
options: {
bootstrap: flags,
baseUrl: 'test-url',
streamUrl: 'test-url',
eventsUrl: 'test-url',
},
ldClient: ldClientMock(flags)
}
```

To configure multiple tests inside a `describe` block you can:
- Follow the method for single test on each test inside the `describe`.
- If all the tests require the same flag configuration place `ldUseClientSpy` at the top of the block in `beforeEach()`.
The two important functions in the `ldCientMock` are `variation` and `allFlags`. These two functions are the ones we use in the app to get feature flags and here we are mocking them with the flag values we define in each test. If we need any other functions in `ldClient` we would just add the mock to `ldClientMock()`.

```javascript
describe('rates across submissions', () => {
beforeEach(() =>
ldUseClientSpy({
'rates-across-submissions': true,
})
)
afterEach(() => {
jest.clearAllMocks()
})

...
const ldClientMock = (featureFlags: FeatureFlagSettings): LDClient => ({
... other functions,
variation: jest.fn(
(
flag: FeatureFlagLDConstant,
defaultValue: FlagValue | undefined
) => featureFlags[flag] ?? defaultValue
),
allFlags: jest.fn(() => featureFlags),
})
```
It's always best to `jest.clearAllMocks()` after each test with either one of these methods, otherwise, preceding tests may have the same flag configured as the previous test.
We define our initial feature flag values in the `flags` variable by combining the default feature flag values with values passed into `renderWithProviders` for each test. Looking at the code snippet below from `renderWithProviders`, we get the default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts) using `getDefaultFeatureFlags()` then merge that with `option.featureFlags` values passed into `renderWithProviders`. This will allow each test to configure the specific feature flag values for that test and supply default values for flags the test did not define.
```typescript
const {
routerProvider = {},
apolloProvider = {},
authProvider = {},
s3Provider = undefined,
location = undefined,
featureFlags = undefined
} = options || {}

const flags = {
...getDefaultFeatureFlags(),
...featureFlags
}

const ldProviderConfig: ProviderConfig = {
clientSideID: 'test-url',
options: {
bootstrap: flags,
baseUrl: 'test-url',
streamUrl: 'test-url',
eventsUrl: 'test-url',
},
ldClient: ldClientMock(flags)
}
```
#### Examples
Using this method in our unit tests is simple and similar to how we configure the other providers. When calling `renderWithProdivers` we need to supply the second argument `options` with the `featureFlag` field.
In the example below we set `featureFlag` with an object that contains two feature flags and their values. When this test is run, the component will be supplied with these two flag values along with the other default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts). Take note that the `featureFlag` field is type `FeatureFlagSettings` so you will only be allowed to define flags that exists in [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts).
```javascript
renderWithProviders(
<ContractDetails
draftSubmission={medicaidAmendmentPackage}
updateDraft={jest.fn()}
previousDocuments={[]}
/>,
{
apolloProvider: {
mocks: [fetchCurrentUserMock({ statusCode: 200 })],
},
featureFlags: {
'rate-edit-unlock': false,
'438-attestation': true
}
}
)
```
### Server side unit testing
LaunchDarkly server side implementation is done by configuring our resolvers with `ldService` dependency. In our resolver we then can use the method `getFeatureFlag` from `ldService` to get the flag value from LaunchDarkly.
Expand Down
16 changes: 16 additions & 0 deletions docs/technical-design/third_party_api_access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Third Party API Access

## Overview
In order to make a request to the API, users (3rd party and non) need to be authenticated (signed in) and authorized (have permission to make a particular request). Non 3rd party users are both authenticated and authorized with Cognito. While 3rd party users will be able to authenticate with cognito, authorization will happen separately that we can grant them longer term credentials for authorization.

## Process for 3rd Parties to Use the MC-Review API

1. 3rd party is authenticated (signs on)
2. 3rd party requests a JWT
3. 3rd party uses the JWT to make a request to the MC-Review API
4. Request is sent to `v1/graphql/external` via the `graphql` API gateway endpoint, which invokes the lambda authorizer
5. The lambda authorizer, `third_party_api_authorizer`, verifies the JWT that the 3rd party sent with their request. If the JWT is valid (valid user, and not expired) the lambda returns an “allow” policy document, otherwise it returns a “deny”. This policy determines if the request can proceed.
6. When authorization is successful the user ID that was granted the JWT is used to fetch a full user record from postgres. This is user is then a part of the context for the resolver.

## JWT Security
Like previously mentioned, third parties will need to have a valid JWT in order to access the MC-Review API. More can be found on JWT security [here](api-jwt-security.md)
71 changes: 23 additions & 48 deletions docs/templates/technical-design-discussion-template.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,54 @@
---
title: Technical Design Discussion Template
title: Technical Discovery Template
---

## Title of Design Doc
# Title of Design Discovery Doc

## Overview

In one or two sentences,
summarize what the problem is and how you intend on solving it.
It's helpful to outline the stakeholders here,
as it informs why this problem merits solving.
In one or two sentences, summarize the problem (may be a feature epic under consideration)

## Links

Jira ticket links or diagrams

## Constraints (optional)

Write what this document will not cover.
Write what this document will not cover

## Terminology (optional)

Define any terminology you are using that could be application system jargon
Define any application system jargon

## Background
## Discovery

This section is for going into more detail about the problem.

- Who are the stakeholders and how have they been impacted?
- Historically, what effect has the problem it had?
- Is there data to illustrate the impact?
- Is there an existing solution?
If so, why does it need to be improved on?
- Are there past projects or designs to link for context?
### What do we need to be able to do?

## Examples
### How should we do it?

At least one example should be used to help the audience understand the context better.
### What relevant code or tooling do we have already?

## Proposed Solution
### What architectural considerations exist?
examples: new or adjusted programming patterns, new or adjusted infrastructure, new libraries introduced, permissions modeling changes, data model changes. Was there a build vs. buy solution? Was there scope considerations or tradeoff?

Detail your solution here.
Start with a broad overview and then go into detail on each portion of the design.
### How will we test this ?

- What changes will the stakeholder/client see?
This should clearly illustrate how the stakeholders' needs are being met
- How exactly does it solve the problem outlined above?
Explain how this solution applies to the use cases identified.
- Why are you picking this solution?
- What are the limits of the proposed solution?
At what point will the design cease to be a viable solution?
- How will you measure the success of your solution?
- If priorities shift or the solution becomes too cumbersome to implement, how will you roll back?

Visual representations of the solution can be helpful here (ex. a diagram for a request lifecycle).

## Implementation
## Proposed Solution

Without going too much into individual tasks,
write an overview of what this solution's implementation would look like.
Detail your solution here. Start with a broad overview and then list the ticket-level tasks as you see them.

- Can this be broken down into multiple technical efforts?
- What is the tech stack involved?
- Will additional infrastructure be needed to achieve this?
- How will you test this?
Visual representations of the solution can be helpful here as well.

## Trade-offs
## Dependencies (optional)

- This section should go over other possible solutions,
and why you chose yours over them.
- Was there a build vs. buy solution?
- What industry standards/practices already exist?
- Why is your solution better?
Are there any dependencies on other feature epics or third party tools? Do we need anything from other teams?

## Cross Team Dependencies (optional)
## Open Questions

If there are multiple teams or clients that are dependencies for implementation to be successful,
list them here.
Examples of this are security or design collaboration/reviews.
Add any open questions still remaining.

## Further Reading

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"devDependencies": {
"@bahmutov/cypress-esbuild-preprocessor": "^2.2.0",
"@cypress-audit/pa11y": "^1.3.0",
"chromedriver": "^120.0.1",
"chromedriver": "^121.0.0",
"cypress": "^12.16.0",
"cypress-pipe": "^2.0.0",
"danger": "^11.2.6",
Expand Down
4 changes: 4 additions & 0 deletions services/app-api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ custom:
reactAppOtelCollectorUrl: ${env:REACT_APP_OTEL_COLLECTOR_URL, ssm:/configuration/react_app_otel_collector_url}
dbURL: ${env:DATABASE_URL}
ldSDKKey: ${env:LD_SDK_KEY, ssm:/configuration/ld_sdk_key_feds}
# because the secret is in JSON in secret manager, we have to pass it into jwtSecret when not running locally
jwtSecretJSON: ${env:CF_CONFIG_IGNORED_LOCALLY, ssm:/aws/reference/secretsmanager/api_jwt_secret_${sls:stage}}
jwtSecret: ${env:JWT_SECRET, self:custom.jwtSecretJSON.jwtsigningkey}
webpack:
webpackConfig: 'webpack.config.js'
packager: 'yarn'
Expand Down Expand Up @@ -154,6 +157,7 @@ provider:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
OPENTELEMETRY_COLLECTOR_CONFIG_FILE: /var/task/collector.yml
LD_SDK_KEY: ${self:custom.ldSDKKey}
JWT_SECRET: ${self:custom.jwtSecret}

layers:
prismaClientMigration:
Expand Down
1 change: 1 addition & 0 deletions services/app-api/src/authn/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type { userFromAuthProvider } from './authn'

export { userFromCognitoAuthProvider, lookupUserAurora } from './cognitoAuthn'
export { userFromThirdPartyAuthorizer } from './thirdPartyAuthn'

export {
userFromLocalAuthProvider,
Expand Down
39 changes: 39 additions & 0 deletions services/app-api/src/authn/thirdPartyAuthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ok, err } from 'neverthrow'
import type { Store } from '../postgres'
import { lookupUserAurora } from './cognitoAuthn'
import { initTracer, recordException } from '../../../uploads/src/lib/otel'

export async function userFromThirdPartyAuthorizer(
store: Store,
userId: string
) {
// setup otel tracing
const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL
if (!otelCollectorURL || otelCollectorURL === '') {
const errMsg =
'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set'
throw errMsg
}

const serviceName = 'third-party-authorizer'
initTracer(serviceName, otelCollectorURL)

try {
// Lookup user from postgres
const auroraUser = await lookupUserAurora(store, userId)
if (auroraUser instanceof Error) {
return err(auroraUser)
}

if (auroraUser === undefined) {
return err(auroraUser)
}

return ok(auroraUser)
} catch (e) {
const err = new Error('ERROR: failed to look up user in postgres')

recordException(err, serviceName, 'lookupUser')
throw err
}
}
Loading

0 comments on commit 68e2515

Please sign in to comment.