Skip to content

Commit

Permalink
feat(lambda): initial user migration trigger support
Browse files Browse the repository at this point in the history
Adds support for the UserMigration_Authentication trigger.
  • Loading branch information
jagregory committed Apr 12, 2020
1 parent 51df572 commit 2f9ecfc
Show file tree
Hide file tree
Showing 25 changed files with 738 additions and 65 deletions.
7 changes: 1 addition & 6 deletions integration-tests/dataStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
CreateDataStore,
createDataStore,
DataStore,
} from "../src/services/dataStore";
import { createUserPool, UserPool } from "../src/services/userPool";
import { createDataStore } from "../src/services/dataStore";
import fs from "fs";
import { promisify } from "util";

Expand Down
17 changes: 10 additions & 7 deletions integration-tests/userPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ describe("User Pool", () => {
);

it("creates a database", async () => {
await createUserPool({ UsernameAttributes: [] }, tmpCreateDataStore);
await createUserPool(
{ UserPoolId: "local", UsernameAttributes: [] },
tmpCreateDataStore
);

expect(fs.existsSync(path + "/local.json")).toBe(true);
});
Expand All @@ -32,7 +35,7 @@ describe("User Pool", () => {
it("saves a user with their username as an additional attribute", async () => {
const now = new Date().getTime();
const userPool = await createUserPool(
{ UsernameAttributes: [] },
{ UserPoolId: "local", UsernameAttributes: [] },
tmpCreateDataStore
);

Expand All @@ -49,7 +52,7 @@ describe("User Pool", () => {
const file = JSON.parse(await readFile(path + "/local.json", "utf-8"));

expect(file).toEqual({
Options: { UsernameAttributes: [] },
Options: { UserPoolId: "local", UsernameAttributes: [] },
Users: {
"1": {
Username: "1",
Expand All @@ -70,7 +73,7 @@ describe("User Pool", () => {
it("updates a user", async () => {
const now = new Date().getTime();
const userPool = await createUserPool(
{ UsernameAttributes: [] },
{ UserPoolId: "local", UsernameAttributes: [] },
tmpCreateDataStore
);

Expand All @@ -88,7 +91,7 @@ describe("User Pool", () => {
let file = JSON.parse(await readFile(path + "/local.json", "utf-8"));

expect(file).toEqual({
Options: { UsernameAttributes: [] },
Options: { UserPoolId: "local", UsernameAttributes: [] },
Users: {
"1": {
Username: "1",
Expand Down Expand Up @@ -119,7 +122,7 @@ describe("User Pool", () => {
file = JSON.parse(await readFile(path + "/local.json", "utf-8"));

expect(file).toEqual({
Options: { UsernameAttributes: [] },
Options: { UserPoolId: "local", UsernameAttributes: [] },
Users: {
"1": {
Username: "1",
Expand All @@ -142,7 +145,7 @@ describe("User Pool", () => {
let userPool: UserPool;
beforeAll(async () => {
userPool = await createUserPool(
{ UsernameAttributes: [] },
{ UserPoolId: "local", UsernameAttributes: [] },
tmpCreateDataStore
);

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@commitlint/config-conventional": "^8.3.4",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@types/aws-lambda": "^8.10.48",
"@types/body-parser": "^1.19.0",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
Expand All @@ -51,6 +52,7 @@
"typescript": "^3.8.3"
},
"dependencies": {
"aws-sdk": "^2.656.0",
"body-parser": "^1.19.0",
"boxen": "^4.2.0",
"cors": "^2.8.5",
Expand Down
21 changes: 21 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ export class InvalidPasswordError extends CognitoError {
}
}

export class PasswordResetRequiredError extends CognitoError {
public constructor() {
super("PasswordResetRequiredException", "Password reset required");
}
}

export class ResourceNotFoundError extends CognitoError {
public constructor() {
super("ResourceNotFoundException", "Resource not found");
}
}

export class UnexpectedLambdaExceptionError extends CognitoError {
public constructor() {
super(
"UnexpectedLambdaExceptionException",
"Unexpected error when invoking lambda"
);
}
}

export const unsupported = (message: string, res: Response) => {
console.error(`Cognito Local unsupported feature: ${message}`);
return res.status(500).json({
Expand Down
15 changes: 13 additions & 2 deletions src/server/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ import { createCodeDelivery } from "../services";
import { ConsoleCodeSender } from "../services/codeDelivery/consoleCodeSender";
import { otp } from "../services/codeDelivery/otp";
import { createDataStore } from "../services/dataStore";
import { createLambda } from "../services/lambda";
import { createTriggers } from "../services/triggers";
import { createUserPool } from "../services/userPool";
import { Router } from "../targets/router";
import { createServer, Server } from "./server";
import * as AWS from "aws-sdk";

export const createDefaultServer = async (): Promise<Server> => {
const dataStore = await createUserPool(
const userPool = await createUserPool(
{
UserPoolId: "local",
UsernameAttributes: ["email"],
},
createDataStore
);
const lambdaClient = new AWS.Lambda({});
const lambda = createLambda({}, lambdaClient);
const triggers = createTriggers({
lambda,
userPool,
});
const router = Router({
codeDelivery: createCodeDelivery(ConsoleCodeSender, otp),
storage: dataStore,
userPool,
triggers,
});

return createServer(router);
Expand Down
4 changes: 3 additions & 1 deletion src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Triggers } from "./triggers";
import { CodeDelivery } from "./codeDelivery/codeDelivery";
import { UserPool } from "./userPool";

export { UserPool } from "./userPool";
export { createCodeDelivery, CodeDelivery } from "./codeDelivery/codeDelivery";

export interface Services {
storage: UserPool;
userPool: UserPool;
codeDelivery: CodeDelivery;
triggers: Triggers;
}
140 changes: 140 additions & 0 deletions src/services/lambda.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { createLambda } from "./lambda";
import * as AWS from "aws-sdk";

describe("Lambda function invoker", () => {
let mockLambdaClient: jest.Mocked<AWS.Lambda>;

beforeEach(() => {
mockLambdaClient = {
invoke: jest.fn(),
} as any;
});

describe("enabled", () => {
it("returns true if lambda is configured", () => {
const lambda = createLambda(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient
);

expect(lambda.enabled("UserMigration")).toBe(true);
});

it("returns false if lambda is not configured", () => {
const lambda = createLambda({}, mockLambdaClient);

expect(lambda.enabled("UserMigration")).toBe(false);
});
});

describe("invoke", () => {
it("throws if lambda is not configured", async () => {
const lambda = createLambda({}, mockLambdaClient);

await expect(
lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
username: "username",
userPoolId: "userPoolId",
})
).rejects.toEqual(new Error("UserMigration trigger not configured"));
});

it("invokes the lambda", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "some": "json" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = createLambda(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient
);

await lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
username: "username",
userPoolId: "userPoolId",
});

expect(mockLambdaClient.invoke).toHaveBeenCalledWith({
FunctionName: "MyLambdaName",
InvocationType: "RequestResponse",
Payload: JSON.stringify({
version: 0,
userName: "username",
callerContext: { awsSdkVersion: "2.656.0", clientId: "clientId" },
region: "local",
userPoolId: "userPoolId",
triggerSource: "UserMigration_Authentication",
request: { userAttributes: {} },
response: {},
}),
});
});

describe("when lambda successful", () => {
it("returns string payload as json", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "response": "value" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = createLambda(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient
);

const result = await lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
username: "username",
userPoolId: "userPoolId",
});

expect(result).toEqual("value");
});

it("returns Buffer payload as json", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: Buffer.from('{ "response": "value" }'),
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = createLambda(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient
);

const result = await lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
username: "username",
userPoolId: "userPoolId",
});

expect(result).toEqual("value");
});
});
});
});
80 changes: 80 additions & 0 deletions src/services/lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { CognitoUserPoolEvent } from "aws-lambda";
import * as AWS from "aws-sdk";
import { UnexpectedLambdaExceptionError } from "../errors";

interface UserMigrationEvent {
userPoolId: string;
clientId: string;
username: string;
password: string;
triggerSource: "UserMigration_Authentication";
}

export type CognitoUserPoolResponse = CognitoUserPoolEvent["response"];

export interface Lambda {
invoke(
lambda: "UserMigration",
event: UserMigrationEvent
): Promise<CognitoUserPoolResponse>;
enabled(lambda: "UserMigration"): boolean;
}

export interface FunctionConfig {
UserMigration?: string;
}

export type CreateLambda = (
config: FunctionConfig,
lambdaClient: AWS.Lambda
) => Lambda;

export const createLambda: CreateLambda = (config, lambdaClient) => ({
enabled: (lambda) => !!config[lambda],
async invoke(lambda, event) {
const lambdaName = config[lambda];
if (!lambdaName) {
throw new Error(`${lambda} trigger not configured`);
}

const lambdaEvent: CognitoUserPoolEvent = {
version: 0, // TODO: how do we know what this is?
userName: event.username,
callerContext: {
awsSdkVersion: "2.656.0", // TODO: this isn't correct
clientId: event.clientId,
},
region: "local", // TODO: pull from above,
userPoolId: event.userPoolId,
triggerSource: event.triggerSource,
request: {
userAttributes: {},
},
response: {},
};

console.log(
`Invoking "${lambdaName}" with event`,
JSON.stringify(lambdaEvent, undefined, 2)
);
const result = await lambdaClient
.invoke({
FunctionName: lambdaName,
InvocationType: "RequestResponse",
Payload: JSON.stringify(lambdaEvent),
})
.promise();

console.log(
`Lambda completed with StatusCode=${result.StatusCode} and FunctionError=${result.FunctionError}`
);
if (result.StatusCode === 200) {
const parsedPayload = JSON.parse(result.Payload as string);

return parsedPayload.response as CognitoUserPoolResponse;
} else {
console.error(result.FunctionError);
throw new UnexpectedLambdaExceptionError();
}
},
});
1 change: 1 addition & 0 deletions src/services/triggers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Triggers, createTriggers } from "./triggers";
Loading

0 comments on commit 2f9ecfc

Please sign in to comment.