Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support api gateway and path based CORS #305

Merged
merged 2 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions packages/@eventual/aws-cdk/src/command-service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {
HttpApi,
HttpRouteProps,
HttpMethod,
HttpRouteProps
CorsHttpMethod,
} from "@aws-cdk/aws-apigatewayv2-alpha";
import { HttpIamAuthorizer } from "@aws-cdk/aws-apigatewayv2-authorizers-alpha";
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
import { ENV_NAMES, sanitizeFunctionName } from "@eventual/aws-runtime";
import {
commandRpcPath,
isDefaultNamespaceCommand
} from "@eventual/core";
import { Arn, aws_iam, Lazy, Stack } from "aws-cdk-lib";
import { commandRpcPath, isDefaultNamespaceCommand } from "@eventual/core";
import { Arn, aws_iam, Duration, Lazy, Stack } from "aws-cdk-lib";
import { Effect, IGrantable, PolicyStatement } from "aws-cdk-lib/aws-iam";
import type { Function, FunctionProps } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
Expand All @@ -20,7 +18,7 @@ import type {
CommandFunction,
InternalCommandFunction,
InternalCommandName,
InternalCommands
InternalCommands,
} from "./build-manifest";
import type { EventService } from "./event-service";
import { grant } from "./grant";
Expand All @@ -37,11 +35,47 @@ export type CommandProps<Service> = {
default?: CommandHandlerProps;
} & Partial<ServiceEntityProps<Service, "Command", CommandHandlerProps>>;

export interface CorsOptions {
/**
* Specifies whether credentials are included in the CORS request.
* @default false
*/
readonly allowCredentials?: boolean;
/**
* Represents a collection of allowed headers.
* @default - No Headers are allowed.
*/
readonly allowHeaders?: string[];
/**
* Represents a collection of allowed HTTP methods.
* OPTIONS will be added automatically.
*
* @default - OPTIONS
*/
readonly allowMethods?: CorsHttpMethod[];
/**
* Represents a collection of allowed origins.
* @default - No Origins are allowed.
*/
readonly allowOrigins?: string[];
/**
* Represents a collection of exposed headers.
* @default - No Expose Headers are allowed.
*/
readonly exposeHeaders?: string[];
/**
* The duration that the browser should cache preflight request results.
* @default Duration.seconds(0)
*/
readonly maxAge?: Duration;
}

export interface CommandsProps<Service = any> extends ServiceConstructProps {
activityService: ActivityService<Service>;
overrides?: CommandProps<Service>;
eventService: EventService;
workflowService: WorkflowService;
cors?: CorsOptions;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow this to be configured per command also?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe later, but API Gateway http doesn't support that. The "custom" approach which doesn't use the infra config would support per operation.

}

/**
Expand Down Expand Up @@ -168,6 +202,17 @@ export class CommandService<Service = any> {
"default",
this.serviceCommands.default
),
corsPreflight: props.cors
? {
...props.cors,
allowMethods: Array.from(
new Set([
...(props.cors.allowMethods ?? []),
CorsHttpMethod.OPTIONS,
])
),
}
: undefined,
});

this.finalize();
Expand Down Expand Up @@ -240,7 +285,11 @@ export class CommandService<Service = any> {
}
if (command.path) {
self.gateway.addRoutes({
path: command.path,
// itty router supports paths in the form /*, but api gateway expects them in the form /{proxy+}
path:
command.path === "*"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this fragile? What about /abc/*

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats fine, would become /abc/{proxy+}

? "/{proxy+}"
: (command.path as string).replace(/\*/g, "{proxy+}"),
methods: [
(command.method as HttpMethod | undefined) ?? HttpMethod.GET,
],
Expand Down
1 change: 0 additions & 1 deletion packages/@eventual/aws-cdk/src/deep-composite-principal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class DeepCompositePrincipal extends CompositePrincipal {
override addToPrincipalPolicy(
statement: PolicyStatement
): AddToPrincipalPolicyResult {
console.log(statement);
const res = this._principals.map((p) => p.addToPrincipalPolicy(statement));
const added = res.every((s) => s.statementAdded);
if (added) {
Expand Down
3 changes: 3 additions & 0 deletions packages/@eventual/aws-cdk/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
CommandService,
Commands,
SystemCommands,
CorsOptions,
} from "./command-service";
import { DeepCompositePrincipal } from "./deep-composite-principal.js";
import { EventService } from "./event-service";
Expand Down Expand Up @@ -94,6 +95,7 @@ export interface ServiceProps<Service = any> {
* Override properties of Subscription Functions within the Service.
*/
subscriptions?: SubscriptionOverrides<Service>;
cors?: CorsOptions;
system?: {
/**
* Configuration properties for the workflow orchestrator
Expand Down Expand Up @@ -239,6 +241,7 @@ export class Service<S = any> extends Construct {
overrides: props.commands,
eventService: this.eventService,
workflowService: workflowService,
cors: props.cors,
...serviceConstructProps,
});
proxyCommandService._bind(this.commandService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export function createCommandWorker({
try {
const response = await router.handle(request);
if (response === undefined) {
if (request.method === "OPTIONS") {
return new HttpResponse(undefined, {
// CORS expects a 204 or 200, using 204 to match API Gateway
// and accurately reflect NO CONTENT
status: 204,
});
}
return new HttpResponse(
`Not Found: ${request.method} ${request.url}`,
{
Expand Down