Skip to content

Commit

Permalink
Merge branch 'main' into mt-scan-from-lambda
Browse files Browse the repository at this point in the history
  • Loading branch information
mojotalantikite committed Feb 15, 2024
2 parents 1f69719 + 3456f22 commit 0cf25c4
Show file tree
Hide file tree
Showing 42 changed files with 1,592 additions and 224 deletions.
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -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 ALLOWED_IP_ADDRESSES='127.0.0.1'
export JWT_SECRET='3fd2e448ed2cec1fa46520f1b64bcb243c784f68db41ea67ef9abc45c12951d3e770162829103c439f01d2b860d06ed0da1a08895117b1ef338f1e4ed176448a' # pragma: allowlist secret

export REACT_APP_OTEL_COLLECTOR_URL='http://localhost:4318/v1/traces'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ tsconfig.tsbuildinfo
.serverless
.eslintcache
/.env
.nx
tests_output
*.log
coverage/
Expand Down
6 changes: 6 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ valid values: A URL where a running app-web can be reached

It's used as the redirects for login by app-web when it configures login via IDM.

### `ALLOWED_IP_ADDRESSES`

Read by `app-api` and Cypress

Third party access to the MC-Review API is restricted by IP address. It must be set to a string that contains a comma separated list of IP address OR it can be set to `ALLOW_ALL` for the dev environment and for testing purposes.

### `APP_VERSION`

Read by `app-api`
Expand Down
6 changes: 4 additions & 2 deletions services/app-api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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}
allowedIpAddresses: ${env:ALLOWED_IP_ADDRESSES, ssm:/configuration/allowed_ip_addresses}
# 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}
Expand Down Expand Up @@ -157,6 +158,7 @@ provider:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
OPENTELEMETRY_COLLECTOR_CONFIG_FILE: /var/task/collector.yml
LD_SDK_KEY: ${self:custom.ldSDKKey}
ALLOWED_IP_ADDRESSES: ${self:custom.allowedIpAddresses}
JWT_SECRET: ${self:custom.jwtSecret}

layers:
Expand All @@ -179,15 +181,15 @@ functions:

third_party_api_authorizer:
handler: src/handlers/third_party_API_authorizer.main

otel:
handler: src/handlers/otel_proxy.main
events:
- http:
path: otel
method: post
cors: true

graphql:
handler: src/handlers/apollo_gql.graphqlHandler
events:
Expand Down
37 changes: 37 additions & 0 deletions services/app-api/src/domain-models/nullstoUndefined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Recursively replaces all nulls with undefineds
GQL return <Maybe> types are T | null instead of T | undefined which match our zod .optional() domain types
This functions allows us convert GQL to zod-friendly types to type match zod and apollo server types
and avoid manual type casting or null coalescing work
Adapted from https://github.com/apollographql/apollo-client/issues/2412
*/

type RecursivelyReplaceNullWithUndefined<T> = T extends null
? undefined
: T extends Date
? T
: {
[K in keyof T]: T[K] extends (infer U)[]
? RecursivelyReplaceNullWithUndefined<U>[]
: RecursivelyReplaceNullWithUndefined<T[K]>
}

export function nullsToUndefined<T>(
obj: T
): RecursivelyReplaceNullWithUndefined<T> {
if (obj === null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined as any
}

// object check based on: https://stackoverflow.com/a/51458052/6489012
if (obj?.constructor.name === 'Object') {
for (const key in obj) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj[key] = nullsToUndefined(obj[key]) as any
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return obj as any
}
49 changes: 43 additions & 6 deletions services/app-api/src/handlers/apollo_gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({

if (authProvider || fromThirdPartyAuthorizer) {
try {
// check if the user is stored in postgres
// going to clean this up, but we need the store in the
// userFetcher to query postgres. This code is a duped.
const dbURL = process.env.DATABASE_URL ?? ''
const secretsManagerSecret =
process.env.SECRETS_MANAGER_SECRET ?? ''
Expand Down Expand Up @@ -116,7 +113,9 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
}
} catch (err) {
console.error('Error attempting to fetch user: ', err)
throw new Error('Log: placing user in gql context failed')
throw new Error(
`Log: placing user in gql context failed, ${err}`
)
}
} else {
throw new Error('Log: no AuthProvider from an internal API user.')
Expand Down Expand Up @@ -148,6 +147,38 @@ function localAuthMiddleware(wrapped: APIGatewayProxyHandler): Handler {
}
}

function ipRestrictionMiddleware(
allowedIps: string
): (wrappedArg: Handler) => Handler {
return function (wrapped: Handler): Handler {
return async function (event, context, completion) {
const ipAddress = event.requestContext.identity.sourceIp
const fromThirdPartyAuthorizer = event.requestContext.path.includes(
'/v1/graphql/external'
)

if (fromThirdPartyAuthorizer) {
const isValidIpAddress =
allowedIps.includes(ipAddress) ||
allowedIps.includes('ALLOW_ALL')

if (!isValidIpAddress) {
return Promise.resolve({
statusCode: 403,
body: `{ "error": IP Address ${ipAddress} is not in the allowed list }\n`,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
})
}
}

return await wrapped(event, context, completion)
}
}
}

// Tracing Middleware
function tracingMiddleware(wrapped: Handler): Handler {
return async function (event, context, completion) {
Expand Down Expand Up @@ -197,6 +228,7 @@ async function initializeGQLHandler(): Promise<Handler> {
const otelCollectorUrl = process.env.REACT_APP_OTEL_COLLECTOR_URL
const parameterStoreMode = process.env.PARAMETER_STORE_MODE
const ldSDKKey = process.env.LD_SDK_KEY
const allowedIpAddresses = process.env.ALLOWED_IP_ADDRESSES
const jwtSecret = process.env.JWT_SECRET

// START Assert configuration is valid
Expand All @@ -212,6 +244,9 @@ async function initializeGQLHandler(): Promise<Handler> {
if (stageName === undefined)
throw new Error('Configuration Error: stage is required')

if (allowedIpAddresses === undefined)
throw new Error('Configuration Error: allowed IP addresses is required')

if (!dbURL) {
throw new Error('Init Error: DATABASE_URL is required to run app-api')
}
Expand Down Expand Up @@ -419,11 +454,13 @@ async function initializeGQLHandler(): Promise<Handler> {

// init tracer and set the middleware. tracer needs to be global.
tracer = createTracer('app-api-' + stageName)
const tracingHandler = tracingMiddleware(handler)
const combinedHandler = ipRestrictionMiddleware(allowedIpAddresses)(
tracingMiddleware(handler)
)

// Locally, we wrap our handler in a middleware that returns 403 for unauthenticated requests
const isLocal = authMode === 'LOCAL'
return isLocal ? localAuthMiddleware(tracingHandler) : tracingHandler
return isLocal ? localAuthMiddleware(combinedHandler) : combinedHandler
}

const handlerPromise = initializeGQLHandler()
Expand Down
4 changes: 1 addition & 3 deletions services/app-api/src/handlers/third_party_API_authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ export const main: APIGatewayTokenAuthorizerHandler = async (
try {
// authentication step for validating JWT token
const userId = jwtLib.userIDFromToken(authToken)

if (userId instanceof Error) {
const msg = 'Invalid auth token'
console.error(msg)
console.error('Invalid auth token')

return generatePolicy(undefined, event)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { formatRateNameDate } from '../../../../app-web/src/common-code/dateHelpers'
import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType'
import type { ProgramArgType } from '../../../../app-web/src/common-code/healthPlanFormDataType'
import type { RateFormDataType } from '../../domain-models/contractAndRates'

const generateRateCertificationName = (
rateFormData: RateFormDataType,
stateCode: string,
stateNumber: number,
statePrograms: ProgramArgType[]
): string => {
const {
rateType,
rateProgramIDs,
amendmentEffectiveDateEnd,
amendmentEffectiveDateStart,
rateDateCertified,
rateDateEnd,
rateDateStart,
} = rateFormData

let rateName = `${packageName(
stateCode,
stateNumber,
rateProgramIDs ?? [],
statePrograms
)}-RATE`
if (rateType === 'NEW' && rateDateStart) {
rateName = rateName.concat(
'-',
formatRateNameDate(rateDateStart),
'-',
formatRateNameDate(rateDateEnd),
'-',
'CERTIFICATION'
)
}

if (rateType === 'AMENDMENT') {
rateName = rateName.concat(
'-',
formatRateNameDate(amendmentEffectiveDateStart),
'-',
formatRateNameDate(amendmentEffectiveDateEnd),
'-',
'AMENDMENT'
)
}

if (rateDateCertified) {
rateName = rateName.concat('-', formatRateNameDate(rateDateCertified))
}
return rateName
}

export { generateRateCertificationName }
43 changes: 36 additions & 7 deletions services/app-api/src/resolvers/rate/submitRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import {
setErrorAttributesOnActiveSpan,
setResolverDetailsOnActiveSpan,
} from '../attributeHelper'
import type { RateFormDataType } from '../../domain-models'
import { isStateUser } from '../../domain-models'
import { logError } from '../../logger'
import { ForbiddenError, UserInputError } from 'apollo-server-lambda'
import { NotFoundError } from '../../postgres'
import { GraphQLError } from 'graphql/index'
import type { LDService } from '../../launchDarkly/launchDarkly'
import { generateRateCertificationName } from './generateRateCertificationName'
import { findStatePrograms } from '../../../../app-web/src/common-code/healthPlanFormDataType/findStatePrograms'
import { nullsToUndefined } from '../../domain-models/nullstoUndefined'

/*
Submit rate will change a draft revision to submitted and generate a rate name if one is missing
Also, if form data is passed in (such as on standalone rate edits) the form data itself will be updated
*/
export function submitRate(
store: Store,
launchDarkly: LDService
Expand Down Expand Up @@ -86,15 +92,33 @@ export function submitRate(
})
}

// prepare to generate rate cert name - either use new form data coming down on submit or unsubmitted submission data already in database
const stateCode = unsubmittedRate.stateCode
const stateNumber = unsubmittedRate.stateNumber
const statePrograms = findStatePrograms(stateCode)
const generatedRateCertName = formData
? generateRateCertificationName(
nullsToUndefined(formData),
stateCode,
stateNumber,
statePrograms
)
: generateRateCertificationName(
draftRateRevision.formData,
stateCode,
stateNumber,
statePrograms
)

// combine existing db draft data with any form data added on submit
// call submit rate handler
const submittedRate = await store.submitRate({
rateID,
submittedByUserID: user.id,
submitReason,
submitReason: submitReason ?? 'Initial submission',
formData: formData
? {
rateType: (formData.rateType ??
undefined) as RateFormDataType['rateType'],
rateType: formData.rateType ?? undefined,
rateCapitationType:
formData.rateCapitationType ?? undefined,
rateDocuments: formData.rateDocuments ?? [],
Expand All @@ -108,8 +132,6 @@ export function submitRate(
amendmentEffectiveDateEnd:
formData.amendmentEffectiveDateEnd ?? undefined,
rateProgramIDs: formData.rateProgramIDs ?? [],
rateCertificationName:
formData.rateCertificationName ?? undefined,
certifyingActuaryContacts:
formData.certifyingActuaryContacts
? formData.certifyingActuaryContacts.map(
Expand Down Expand Up @@ -140,7 +162,14 @@ export function submitRate(
actuaryCommunicationPreference:
formData.actuaryCommunicationPreference ?? undefined,
packagesWithSharedRateCerts:
formData.packagesWithSharedRateCerts ?? [],
formData.packagesWithSharedRateCerts.map((pkg) => ({
packageName: pkg.packageName ?? undefined,
packageId: pkg.packageId ?? undefined,
packageStatus: pkg.packageStatus ?? undefined,
})),
rateCertificationName:
formData.rateCertificationName ??
generatedRateCertName,
}
: undefined,
})
Expand Down
2 changes: 2 additions & 0 deletions services/app-api/src/testHelpers/gqlHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ const createAndUpdateTestHealthPlanPackage = async (
rateProgramIDs: [ratePrograms.reverse()[0].id],
actuaryContacts: [
{
id: '123-abc',
name: 'test name',
titleRole: 'test title',
email: 'email@example.com',
Expand All @@ -275,6 +276,7 @@ const createAndUpdateTestHealthPlanPackage = async (
]
draft.addtlActuaryContacts = [
{
id: '123-addtl-abv',
name: 'test name',
titleRole: 'test title',
email: 'email@example.com',
Expand Down
2 changes: 2 additions & 0 deletions services/app-graphql/src/mutations/submitRate.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ mutation submitRate($input: SubmitRateInput!) {
rateProgramIDs,
rateCertificationName,
certifyingActuaryContacts {
id
name
titleRole
email
actuarialFirm
actuarialFirmOther
},
addtlActuaryContacts {
id
name
titleRole
email
Expand Down
2 changes: 2 additions & 0 deletions services/app-graphql/src/mutations/unlockRate.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ fragment rateRevisionFragment on RateRevision {
rateProgramIDs,
rateCertificationName,
certifyingActuaryContacts {
id
name
titleRole
email
actuarialFirm
actuarialFirmOther
},
addtlActuaryContacts {
id
name
titleRole
email
Expand Down
Loading

0 comments on commit 0cf25c4

Please sign in to comment.