Skip to content

Commit

Permalink
Admin demo tool (#127)
Browse files Browse the repository at this point in the history
* Add test & debug tool endpoint with admin authentication

* Add test for query without building vs query with empty building
  • Loading branch information
keichan34 authored Apr 16, 2021
1 parent bb01c37 commit fcd2ac0
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 23 deletions.
15 changes: 15 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ functions:
path: '/admin/keys'
method: post
cors: *default_cors

- http:
path: '/admin/keys/{keyId}/reissue'
method: patch
Expand All @@ -226,6 +227,20 @@ functions:
keyId: true
cors: *default_cors

- http:
path: '/admin/query'
method: get
cors: *default_cors

- http:
path: '/admin/query/{estateId}'
method: get
request:
parameters:
paths:
estateId: true
cors: *default_cors

demo:
handler: src/demo.handler
events:
Expand Down
15 changes: 13 additions & 2 deletions src/admin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import '.'
import Sentry from './lib/sentry'
import { APIGatewayProxyHandler } from 'aws-lambda'
import { APIGatewayProxyResult, Handler } from 'aws-lambda'
import { errorResponse } from './lib/proxy-response'
import jwt from "jsonwebtoken"
import jwks from "jwks-rsa"

import * as keys from "./admin/keys"
import { _handler as publicHandler } from './public'
import { _handler as idQueryHandler } from './idQuery'

import { decapitalize } from './lib'
import { AUTH0_DOMAIN, AUTH0_MGMT_DOMAIN } from './lib/auth0_client'

Expand All @@ -17,7 +20,7 @@ const jwksClient = jwks({
jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`
})

const _handler: APIGatewayProxyHandler = async (event) => {
const _handler: Handler<PublicHandlerEvent, APIGatewayProxyResult> = async (event, context, callback) => {
const headers = decapitalize(event.headers)
const tokenHeader = headers['authorization']
if (!tokenHeader || !tokenHeader.match(/^bearer /i)) {
Expand Down Expand Up @@ -68,6 +71,14 @@ const _handler: APIGatewayProxyHandler = async (event) => {
return keys.create(adminEvent)
} else if (event.resource === "/admin/keys/{keyId}/reissue" && event.httpMethod === "PATCH") {
return keys.reissue(adminEvent)
} else if (event.resource === "/admin/query" && event.httpMethod === "GET") {
event.preauthenticatedUserId = userId
event.isDebugMode = event.queryStringParameters?.debug === 'true'
return await publicHandler(event, context, callback) as APIGatewayProxyResult
} else if (event.resource === "/admin/query/{estateId}" && event.httpMethod === "GET") {
event.preauthenticatedUserId = userId
event.isDebugMode = event.queryStringParameters?.debug === 'true'
return await idQueryHandler(event, context, callback) as APIGatewayProxyResult
}
return errorResponse(404, 'Not found')
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"

declare global {
interface PublicHandlerEvent extends APIGatewayProxyEvent {
preauthenticatedUserId?: string
isDemoMode?: boolean
isDebugMode?: boolean
}
Expand Down
15 changes: 12 additions & 3 deletions src/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ export const extractApiKey = (event: PublicHandlerEvent) => {
const apiKey = event.queryStringParameters ? event.queryStringParameters['api-key'] : undefined
const accessToken = decapitalize(event.headers)['x-access-token']

return {
const resp = {
apiKey,
accessToken
}

if (typeof event.preauthenticatedUserId !== 'undefined') {
resp.accessToken = "XXX"
}

return resp
}

export type AuthenticationPlanIdentifier = "paid" | "free"
Expand All @@ -23,15 +29,18 @@ export type AuthenticationResult = {
export const authenticateEvent = async (event: PublicHandlerEvent, quotaType: string): Promise<APIGatewayProxyResult | AuthenticationResult> => {
const { apiKey, accessToken } = extractApiKey(event)

// authentication is skipped when in demo mode
if (event.isDemoMode) {
return { valid: true, plan: "paid" }
}

// authentication is skipped when in demo mode
if (!apiKey || !accessToken) {
return errorResponse(403, 'Incorrect querystring parameter `api-key` or `x-access-token` header value.')
}
const authenticateResult = await authenticate(apiKey, accessToken)

// if preauthenticated is true, then skip access token check
// preauthenticated is set to true when going through the admin console
const authenticateResult = await authenticate(apiKey, accessToken, event.preauthenticatedUserId)
if (authenticateResult.authenticated === false) {
return errorResponse(403, 'Incorrect querystring parameter `api-key` or `x-access-token` header value.')
}
Expand Down
31 changes: 16 additions & 15 deletions src/lib/dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ type AuthenticationResult = {
plan: AuthenticationPlanIdentifier
}

export const authenticate = async (apiKey: string, accessToken: string): Promise<AuthenticationResult> => {
export const authenticate = async (
apiKey: string,
accessToken: string,
preauthenticatedUserId?: string,
): Promise<AuthenticationResult> => {
const getItemInput: AWS.DynamoDB.DocumentClient.GetItemInput = {
TableName: process.env.AWS_DYNAMODB_API_KEY_TABLE_NAME,
Key: { apiKey }
Expand All @@ -92,22 +96,19 @@ export const authenticate = async (apiKey: string, accessToken: string): Promise
}
}

if ('hashedToken' in item && item.hashedToken === await hashTokenV2(apiKey, accessToken)) {
return {
authenticated: true,
lastRequestAt: item.lastRequestAt,
customQuotas,
plan: item.plan || "paid",
}
const authenticatedResp = {
authenticated: true,
lastRequestAt: item.lastRequestAt,
customQuotas,
plan: item.plan || "paid",
}

if ('accessToken' in item && item.accessToken === hashToken(accessToken)) {
return {
authenticated: true,
lastRequestAt: item.lastRequestAt,
customQuotas,
plan: item.plan || "paid",
}
if (
(typeof preauthenticatedUserId !== 'undefined' && item.GSI1PK === preauthenticatedUserId) ||
('hashedToken' in item && item.hashedToken === await hashTokenV2(apiKey, accessToken)) ||
('accessToken' in item && item.accessToken === hashToken(accessToken))
) {
return authenticatedResp
}

return { authenticated: false }
Expand Down
104 changes: 104 additions & 0 deletions src/public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,110 @@ test('should get estate ID with details if authenticated', async () => {
])
})

test('should return the same ID for query with empty building', async () => {
const event1 = {
isDemoMode: true,
queryStringParameters: {
q: '岩手県盛岡市盛岡駅西通2丁目9番地2号',
},
}
const event2 = {
isDemoMode: true,
queryStringParameters: {
q: '岩手県盛岡市盛岡駅西通2丁目9番地2号',
building: '',
},
}
// @ts-ignore
const lambdaResult1 = await handler(event1) as APIGatewayProxyResult
const body1 = JSON.parse(lambdaResult1.body)

// @ts-ignore
const lambdaResult2 = await handler(event2) as APIGatewayProxyResult
const body2 = JSON.parse(lambdaResult2.body)

expect(body1[0].ID).toEqual(body2[0].ID)
})

describe("preauthenticatedUserId", () => {
test('should get estate ID if preauthenticated', async () => {
const userId = 'keymock|should get estate ID if preauthenticated'
const { apiKey } = await dynamodb.createApiKey('should get estate ID if preauthenticated', {
GSI1PK: userId,
plan: "free",
})
const event = {
queryStringParameters: {
q: '岩手県盛岡市盛岡駅西通2丁目9番地1号',
building: 'マリオス10F',
'api-key': apiKey,
},
preauthenticatedUserId: userId,
}
// @ts-ignore
const lambdaResult = await handler(event) as APIGatewayProxyResult
const body = JSON.parse(lambdaResult.body)

expect(lambdaResult.statusCode).toBe(200)
expect(body[0].ID).toBeDefined()
expect(body[0].location).toBeUndefined()
})

test('should get estate ID if preauthenticated as paid user', async () => {
const userId = 'keymock|should get estate ID if preauthenticated as paid user'
const { apiKey } = await dynamodb.createApiKey('should get estate ID if preauthenticated as paid user', {
GSI1PK: userId,
plan: "paid",
})
const event = {
queryStringParameters: {
q: '岩手県盛岡市盛岡駅西通2丁目9番地1号',
building: 'マリオス10F',
'api-key': apiKey,
},
preauthenticatedUserId: userId,
}
// @ts-ignore
const lambdaResult = await handler(event) as APIGatewayProxyResult
const body = JSON.parse(lambdaResult.body)

expect(lambdaResult.statusCode).toBe(200)
expect(body[0].ID).toBeDefined()
expect(body[0].location).toMatchObject({
"geocoding_level": "8",
"lat": "39.701281",
"lng": "141.13366",
})
})

test('should get error if API key and preauthenticated user does not match', async () => {
const userId = 'keymock|should get error if API key and preauthenticated user does not match'
const userId2 = 'keymock|should get error if API key and preauthenticated user does not match 2'
const { apiKey } = await dynamodb.createApiKey('should get error if API key and preauthenticated user does not match', {
GSI1PK: userId,
plan: "free",
})
await dynamodb.createApiKey('should get error if API key and preauthenticated user does not match2', {
GSI1PK: userId2,
plan: "free",
})
const event = {
queryStringParameters: {
q: '岩手県盛岡市盛岡駅西通2丁目9番地1号',
building: 'マリオス10F',
'api-key': apiKey,
},
preauthenticatedUserId: userId2,
}
// @ts-ignore
const lambdaResult = await handler(event) as APIGatewayProxyResult
const body = JSON.parse(lambdaResult.body)

expect(lambdaResult.statusCode).toBe(403)
expect(body[0]?.ID).toBeUndefined()
})
})

test('[Not Recommended request type] should get estate ID with details if authenticated and Building name in q query. ', async () => {
const { apiKey, accessToken } = await dynamodb.createApiKey('should get estate ID with details if authenticated')
const event = {
Expand Down
4 changes: 2 additions & 2 deletions src/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createLog } from './lib/dynamodb_logs'

export const _handler: Handler<PublicHandlerEvent, APIGatewayProxyResult> = async (event) => {
const address = event.queryStringParameters?.q
const building = event.queryStringParameters && event.queryStringParameters['building'] ? event.queryStringParameters['building'] : undefined
const building = event.queryStringParameters?.building
const ZOOM = parseInt(process.env.ZOOM, 10)
const quotaType = "id-req"

Expand Down Expand Up @@ -146,7 +146,7 @@ export const _handler: Handler<PublicHandlerEvent, APIGatewayProxyResult> = asyn
body = { ID }
}

if (event.isDebugMode && event.isDemoMode) {
if (event.isDebugMode === true) {
// aggregate debug info
return json({
internallyNormalized: prenormalizedAddress,
Expand Down

0 comments on commit fcd2ac0

Please sign in to comment.