Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into MCR-3777-single-rate
Browse files Browse the repository at this point in the history
  • Loading branch information
haworku committed Feb 6, 2024
2 parents 9a6e986 + a22f330 commit 93997e6
Show file tree
Hide file tree
Showing 28 changed files with 1,147 additions and 203 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/deploy-infra-to-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ jobs:
stage-name: ${{ inputs.stage_name }}
changed-services: ${{ inputs.changed_services }}

- uses: actions/download-artifact@v4
with:
name: lambda-layers-clamav
path: ./services/uploads/lambda-layers-clamav

- name: Unzip clamav layer
run: |
unzip -d ./services/uploads/lambda-layers-clamav ./services/uploads/lambda-layers-clamav/lambda_layer.zip
rm -f ./services/uploads/lambda-layers-clamav/lambda_layer.zip
- name: deploy uploads
id: deploy-uploads
env:
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,29 @@ jobs:
name: lambda-layers-prisma-client-engine
path: ./services/app-api/lambda-layers-prisma-client-engine

build-clamav-layer:
name: build - clamav layer
runs-on: ubuntu-20.04
steps:
- name: Check out repository
uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn

- name: Prepare ClamAV layer
working-directory: services/uploads/src/avLayer
run: ./dockerbuild.sh

- uses: actions/upload-artifact@v4
with:
name: lambda-layers-clamav
path: ./services/uploads/src/avLayer/build/lambda_layer.zip

deploy-infra:
needs: [begin-deployment]
needs: [begin-deployment, build-clamav-layer]
uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@main
with:
environment: dev
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,29 @@ jobs:
name: lambda-layers-prisma-client-engine
path: ./services/app-api/lambda-layers-prisma-client-engine

build-clamav-layer:
name: build - clamav layer
runs-on: ubuntu-20.04
steps:
- name: Check out repository
uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn

- name: Prepare ClamAV layer
working-directory: services/uploads/src/avLayer
run: ./dockerbuild.sh

- uses: actions/upload-artifact@v4
with:
name: lambda-layers-clamav
path: ./services/uploads/src/avLayer/build/lambda_layer.zip

promote-infra-dev:
needs: [build-prisma-client-lambda-layer, unit-tests]
needs: [build-prisma-client-lambda-layer, build-clamav-layer, unit-tests]
uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@main
with:
environment: dev
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
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
}
}
30 changes: 25 additions & 5 deletions services/app-api/src/handlers/apollo_gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { userFromAuthProvider } from '../authn'
import {
userFromCognitoAuthProvider,
userFromLocalAuthProvider,
userFromThirdPartyAuthorizer,
} from '../authn'
import { newLocalEmailer, newSESEmailer } from '../emailer'
import { NewPostgresStore } from '../postgres/postgresStore'
Expand Down Expand Up @@ -59,10 +60,17 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyContext = context as any
const requestSpan = anyContext[requestSpanKey]

const authProvider =
event.requestContext.identity.cognitoAuthenticationProvider
if (authProvider) {
// This handler is shared with the third_party_API_authorizer
// when called from the 3rd party authorizer the cognito auth provider
// is not valid for instead the authorizer returns a user ID
// that is used to fetch the user
const fromThirdPartyAuthorizer = event.requestContext.path.includes(
'/v1/graphql/external'
)

if (authProvider || fromThirdPartyAuthorizer) {
try {
// check if the user is stored in postgres
// going to clean this up, but we need the store in the
Expand All @@ -81,8 +89,21 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
}

const store = NewPostgresStore(pgResult)
const userResult = await userFetcher(authProvider, store)
const userId = event.requestContext.authorizer?.principalId

let userResult
if (authProvider && !fromThirdPartyAuthorizer) {
userResult = await userFetcher(authProvider, store)
} else if (fromThirdPartyAuthorizer && userId) {
userResult = await userFromThirdPartyAuthorizer(
store,
userId
)
}

if (userResult === undefined) {
throw new Error(`Log: userResult must be supplied`)
}
if (!userResult.isErr()) {
return {
user: userResult.value,
Expand All @@ -98,7 +119,7 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
throw new Error('Log: placing user in gql context failed')
}
} else {
throw new Error('Log: no AuthProvider')
throw new Error('Log: no AuthProvider from an internal API user.')
}
}
}
Expand Down Expand Up @@ -132,7 +153,6 @@ function tracingMiddleware(wrapped: Handler): Handler {
return async function (event, context, completion) {
// get the parent context from headers
const ctx = propagation.extract(ROOT_CONTEXT, event.headers)

const span = tracer.startSpan(
'handleRequest',
{
Expand Down
3 changes: 3 additions & 0 deletions services/app-web/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const ROUTES = [
'DASHBOARD_SUBMISSIONS',
'DASHBOARD_RATES',
'GRAPHQL_EXPLORER',
'API_ACCESS',
'HELP',
'SETTINGS',
'RATES_SUMMARY',
Expand Down Expand Up @@ -45,6 +46,7 @@ const RoutesRecord: Record<RouteT, string> = {
DASHBOARD_SUBMISSIONS: '/dashboard/submissions',
DASHBOARD_RATES: '/dashboard/rate-reviews',
GRAPHQL_EXPLORER: '/dev/graphql-explorer',
API_ACCESS: '/dev/api-access',
HELP: '/help',
SETTINGS: '/settings',
RATES_SUMMARY: '/rates/:id',
Expand Down Expand Up @@ -118,6 +120,7 @@ const PageTitlesRecord: Record<RouteT | 'UNKNOWN_ROUTE', string> = {
ROOT: 'Home',
AUTH: 'Login',
GRAPHQL_EXPLORER: 'GraphQL explorer',
API_ACCESS: 'API Access',
HELP: 'Help',
SETTINGS: 'Settings',
DASHBOARD: 'Dashboard',
Expand Down
1 change: 1 addition & 0 deletions services/app-web/src/constants/tealium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const CONTENT_TYPE_BY_ROUTE: Record<RouteT | 'UNKNOWN_ROUTE', string> = {
DASHBOARD_RATES: 'table',
HELP: 'glossary',
GRAPHQL_EXPLORER: 'dev',
API_ACCESS: 'dev',
SETTINGS: 'table',
RATES_SUMMARY: 'summary',
RATE_EDIT: 'form',
Expand Down
2 changes: 1 addition & 1 deletion services/app-web/src/gqlHelpers/apolloErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const handleNetworkError = (
}

const handleGQLErrors = (graphQLErrors: GraphQLErrors) => {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
graphQLErrors.forEach(({ message, locations, path }) => {
recordJSException(
// Graphql errors mean something is wrong inside our api, maybe bad request or errors we return from api for known edge cases
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
Expand Down
13 changes: 10 additions & 3 deletions services/app-web/src/hooks/useTealium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,22 @@ const useTealium = (): {
// Add page view
// this effect should fire on each page view or if something changes about logged in user
useEffect(() => {

// Do not add tealium for local dev or review apps
if (process.env.REACT_APP_AUTH_MODE !== 'IDM') {
// console.info(`mock tealium page view: ${tealiumPageName}`)
return
}

// Guardrail - protect against trying to call utag before its loaded.
const waitForUtag = async () => {
return new Promise(resolve => setTimeout(resolve, 1000));
}


if (!window.utag) {
return
waitForUtag().catch(() => { /* All of this is a guardrail - protect against trying to call utag before its loaded*/ })
if (!window.utag) {
return
}
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
Expand Down
Loading

0 comments on commit 93997e6

Please sign in to comment.