Skip to content

Commit

Permalink
feat: #3617 add gql authorizer (#3676)
Browse files Browse the repository at this point in the history
* feat: #3617 add gql authorizer

* fix: #3617 updates cloud alert to new events service endpoint

* fix: broken test
  • Loading branch information
willmcvay authored Mar 8, 2021
1 parent 826cb46 commit 405486d
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 22 deletions.
2 changes: 1 addition & 1 deletion packages/cloud-alert/cypress/support/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const webAppsProd = [
]

export const nodeServicesDev = [
{ appName: 'Event Status Service', url: 'https://qgbtloumi8.execute-api.eu-west-2.amazonaws.com/dev' },
{ appName: 'Events Service', url: 'https://kzekq19177.execute-api.eu-west-2.amazonaws.com/dev' },
{ appName: 'Mailer Service', url: 'https://ew9zhgy2i1.execute-api.eu-west-2.amazonaws.com/dev' },
{ appName: 'Payment Service', url: 'https://cja5ya4nc1.execute-api.eu-west-2.amazonaws.com/dev' },
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const confirmRegistrationTemplate = ({ userName, url }: ConfirmPasswordTe
<p>Reapit Connect is our single sign on solution which allows you to seamlessly access products and services provided by Reapit Ltd.</p>
<p>Please see below your username and verification code. When you click the verification link, you will be re-directed to a screen where you will be asked to change your password.</p>
<div style="text-align: center;">
<a style="border: solid thin #0061a8;padding: 10px;background-color: #0061a8;color: white;z-index: 1;text-decoration: none; font-style: normal;" href=<${url}?userName=${userName}&verificationCode={####}>VERIFY ACCOUNT</a>
<a style="border: solid thin #0061a8;padding: 10px;background-color: #0061a8;color: white;z-index: 1;text-decoration: none; font-style: normal;" href=${url}?userName=${userName}&verificationCode={####}>VERIFY ACCOUNT</a>
</div>
<p>Once your account has been verified, you will be redirected to the login page.</p>
<p>Best Regards,</p>
Expand All @@ -44,7 +44,7 @@ export const adminUserInviteTemplate = ({ userName, url }: ConfirmPasswordTempla
<img style="width: 25%; margin: 0 auto; padding: 16px; display: block;" src="https://web-components.prod.paas.reapit.cloud/reapit-connect.jpeg" />
<h1 style="text-align: center;font-size: 24px; font-style: normal; padding:0 16px 24px 16px;">Welcome to Reapit Connect</h1>
<div style="padding:0 16px 16px 16px;">
<p>Hi <%= userName %>,</p>
<p>Hi ${userName},</p>
<p>Welcome to Reapit Connect.</p>
<p>Reapit Connect is our single sign on solution which allows you to seamlessly access products and services provided by Reapit Ltd.</p>
<p>Please see below your username and temporary password. When you click the login link, you will be re-directed to a screen where you will be asked to change your password.</p>
Expand Down
5 changes: 3 additions & 2 deletions packages/geo-diary/src/graphql/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { notification } from '@reapit/elements'
import { ReapitConnectSession } from '@reapit/connect-session'

export const generateRequest = (session: ReapitConnectSession) => async (operation: Operation) => {
const { loginIdentity, accessToken } = session
const { loginIdentity, accessToken, idToken } = session
operation.setContext({
headers: {
authorization: accessToken,
authorization: idToken,
'reapit-connect-token': `Bearer ${accessToken}`,
'reapit-customer': `${loginIdentity.clientId}-${loginIdentity.userCode}`,
},
})
Expand Down
36 changes: 34 additions & 2 deletions packages/graphql-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

![lines](./src/tests/badges/badge-lines.svg) ![functions](./src/tests/badges/badge-functions.svg) ![branches](./src/tests/badges/badge-branches.svg) ![statements](./src/tests/badges/badge-statements.svg)

A GraphQL implementation of the Foundations API.
A GraphQL implementation of the Foundations API. The GraphQL service is a proxy service that sits on top of the Reapit Foundations Platform API. The service exists as a convenience for developers to build web applications quickly and with minimal boilerplate.

Under active development pre-alpha.
**The service under active development - if you are interested in becoming an Alpha tester, please contact Will McVay [email protected]**

## Basic Usage

The GraphQL production service is deployed to `https://graphql.reapit.cloud/graphql`

Loading this Url in your browser will do a get to the service and load the GraphQL playground https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/ where you can experiment with the service, like Swagger but for GraphQL.

There are two required headers in the service as per below, you will need to obtain your access and id tokens from your Reapit Connect session as normal. These headers need to be added to the http headers section of the playground for you to interact with the service.

Naturally, the scopes in the access token provided in the reapit-connect-token header will need to be sufficient to map to the platform endpoints downstream from GraphQL.

```json
{
"authorization": "id-token-from-reapit-connect",
"reapit-connect-token": "access-token-from-reapit-connect"
}

```

Internally we use Apollo Client to query the Apollo Server backend (this service). An example of how we set this up and add a query provider to a Reapit Scaffolded app can be found here and here.

## Errors

Because the service is a platform proxy, it is very likely that 4xx errors are downstream platform issues rather than errors thrown by the service itself. The exception to this rule are 401 errors which are thrown by our API gateway owing to an invalid or missing idToken. See previous point regarding headers.

For downstream errors, the service will return a 200 with an errors object in the payload. You should inspect this errors list for details of any failures downstream, even if the GraphQL service itself was able to respond correctly.

## Known Limitations During Alpha

- The service is already working in production with live customers, powering the Reapit Geo Diary app however, naturally with Alpha software there will likely be some bugs. If you find an issue you believe to be problem with the GraphQL service, please open an issue here https://github.com/reapit/foundations/issues/new?assignees=&labels=bug%2C+needs-triage%2C+graphql-server&template=bug_report.md&title= and we will look at it as soon as possible.
- The objective of the project is to offer the an identical schema to the Foundations API, with no extension, modification or deviation. Objects returned from the service should map 1:1 to those seen in the developer portal swagger document https://developers.reapit.cloud/swagger. That said, there may be some lag between when a property is updated in the main platform. If you find any inconsistencies between the platform API and the service, again please report as an issue - it is likely that the platform has updated and the service is yet to follow. We will be conducting a review of all services before releasing to Beta production to esure full schema compliance.
- The service is lacking two services that exist in the Platform Schema - Meta Data and Meta Data Schema. If there is a requirement to add these services in the near term, please comment on this issue and we will look at prioritising https://github.com/reapit/foundations/issues/3631
7 changes: 5 additions & 2 deletions packages/graphql-server/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resources:
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,reapit-customer,api-version,if-match'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,reapit-customer,reapit-connect-token,api-version,if-match'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
Expand All @@ -48,7 +48,7 @@ resources:
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,reapit-customer,api-version,if-match'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,reapit-customer,reapit-connect-token,api-version,if-match'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'
Expand Down Expand Up @@ -88,8 +88,11 @@ functions:
- X-Amz-Security-Token
- X-Amz-User-Agent
- reapit-customer
- reapit-connect-token
- api-version
- if-match
authorizer:
arn: arn:aws:cognito-idp:${self:provider.region}:${self:custom.env.AWS_ACCOUNT_ID}:userpool/${self:custom.env.COGNITO_USERPOOL_ID}
- http:
path: graphql
method: get
Expand Down
4 changes: 2 additions & 2 deletions packages/graphql-server/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('index.js', () => {
const mockParams = {
event: {
headers: {
Authorization: 'Mock Authorization',
'reapit-connect-token': 'Mock Authorization',
},
},
context: {
Expand All @@ -45,7 +45,7 @@ describe('index.js', () => {
authorization: 'Mock Authorization',
functionName: 'Mock Function Name',
headers: {
Authorization: 'Mock Authorization',
'reapit-connect-token': 'Mock Authorization',
},
event: mockParams.event,
context: mockParams.context,
Expand Down
28 changes: 22 additions & 6 deletions packages/graphql-server/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { AuthenticationError, UserInputError, ForbiddenError, ValidationError, A
import logger from '../logger'

export const errorMessages = {
notAuthorized: '[E4010] Not Authorized',
badRequest: '[E4000] Bad Request',
notFound: '[E4040] Not Found',
forbidden: '[E4030] Forbidden',
internalServer: '[E5000] Internal Server Error',
notAuthorized: '401 - Not Authorized',
badRequest: '400 - Bad Request',
notFound: '404 - Not Found',
forbidden: '403 - Forbidden',
precondion: '412 - Precondition Failed',
unprocessable: '422 - Unprocessable Entity',
internalServer: '500 - Internal Server Error',
}

export const generateAuthenticationError = (traceId?: string) => {
Expand All @@ -21,6 +23,18 @@ export const generateUserInputError = (traceId?: string) => {
return error
}

export const generateUnprocessableError = (traceId?: string) => {
const error = new UserInputError(`${traceId || ''} - ${errorMessages.unprocessable}`)
logger.info('generateUnprocessableError', { traceId, error: JSON.stringify(error) })
return error
}

export const generatePreconditionError = (traceId?: string) => {
const error = new UserInputError(`${traceId || ''} - ${errorMessages.precondion}`)
logger.info('generatePreconditionError', { traceId, error: JSON.stringify(error) })
return error
}

export const generateValidationError = (traceId?: string) => {
const error = new ValidationError(`${traceId || ''} - ${errorMessages.badRequest}`)
logger.info('generateValidationError', { traceId, error: JSON.stringify(error) })
Expand All @@ -44,8 +58,10 @@ export const generateNotFoundError = (traceId?: string) => {

const errors = {
generateAuthenticationError,
generateUserInputError,
generateUnprocessableError,
generateValidationError,
generateUserInputError,
generatePreconditionError,
generateForbiddenError,
generateInternalServerError,
generateNotFoundError,
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const handleContext = ({ event, context }) => {
const newContext = {
traceId: traceId,
headers: event.headers,
authorization: event?.headers?.Authorization || '',
authorization: event?.headers['reapit-connect-token'] ?? '',
functionName: context.functionName,
event,
context,
Expand Down
23 changes: 21 additions & 2 deletions packages/graphql-server/src/utils/__tests__/handle-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jest.mock('apollo-server-lambda', () => {
}
})
jest.mock('../../logger/')

describe('handleError', () => {
it('should return ValidationError', async () => {
const input = {
Expand All @@ -28,6 +29,22 @@ describe('handleError', () => {
expect(output).toEqual(errors.generateValidationError('mockTraceId'))
})

it('should return ValidationError', async () => {
const input = {
error: {
response: {
data: {},
status: 400,
headers: {},
},
},
caller: 'mockCaller',
traceId: 'mockTraceId',
} as HandleErrorParams
const output = await handleError(input)
expect(output).toEqual(errors.generateValidationError('mockTraceId'))
})

it('should return AuthenticationError', async () => {
const input = {
error: {
Expand Down Expand Up @@ -89,7 +106,7 @@ describe('handleError', () => {
traceId: 'mockTraceId',
} as HandleErrorParams
const output = await handleError(input)
expect(output).toEqual(errors.generateUserInputError('mockTraceId'))
expect(output).toEqual(errors.generateUnprocessableError('mockTraceId'))
})

it('should return UserInputError', async () => {
Expand All @@ -105,7 +122,7 @@ describe('handleError', () => {
traceId: 'mockTraceId',
} as HandleErrorParams
const output = await handleError(input)
expect(output).toEqual(errors.generateUserInputError('mockTraceId'))
expect(output).toEqual(errors.generatePreconditionError('mockTraceId'))
})

it('should return ApolloError', async () => {
Expand Down Expand Up @@ -133,6 +150,7 @@ describe('handleError', () => {
const output = await handleError(input)
expect(output).toEqual(errors.generateInternalServerError('mockTraceId'))
})

it('should return ApolloError', async () => {
const input = {
error: {},
Expand All @@ -142,6 +160,7 @@ describe('handleError', () => {
const output = await handleError(input)
expect(output).toEqual(errors.generateInternalServerError('mockTraceId'))
})

it('should return ApolloError', async () => {
const input = {
error: {
Expand Down
4 changes: 2 additions & 2 deletions packages/graphql-server/src/utils/handle-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ export const handleError = async ({ error, traceId, caller }: HandleErrorParams)
return errors.generateNotFoundError(traceId)
}
if (error?.response?.status === 412) {
return errors.generateUserInputError(traceId)
return errors.generatePreconditionError(traceId)
}
if (error?.response?.status === 422) {
return errors.generateUserInputError(traceId)
return errors.generateUnprocessableError(traceId)
}
return errors.generateInternalServerError(traceId)
}
Expand Down

0 comments on commit 405486d

Please sign in to comment.