Skip to content

Commit

Permalink
Merge pull request #44 from supabase-community/refactor-smtp-and-jwt
Browse files Browse the repository at this point in the history
chore: Refactoring
  • Loading branch information
mats16 authored Jun 16, 2023
2 parents 17effb4 + dac8130 commit 27e7c3e
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 367 deletions.
113 changes: 0 additions & 113 deletions src/amazon-ses-smtp.ts

This file was deleted.

File renamed without changes.
100 changes: 100 additions & 0 deletions src/amazon-ses-smtp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import * as cr from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { WorkMailStack } from '../aws-workmail';

interface SesSmtpProps {
region: string;
email: string;
workMailEnabled: cdk.CfnCondition;
}

export class SesSmtp extends Construct {
secret: Secret;
host: string;
port: number;
email: string;

constructor(scope: Construct, id: string, props: SesSmtpProps) {
super(scope, id);

const { region, email, workMailEnabled } = props;

/** IAM Policy to send email via Amazon SES */
const sendEmailPolicy = new iam.Policy(this, 'SendEmailPolicy', {
statements: [
new iam.PolicyStatement({
actions: ['ses:SendRawEmail'],
resources: ['*'],
}),
],
});

/** IAM User to send email via Amazon SES */
const user = new iam.User(this, 'User');
user.attachInlinePolicy(sendEmailPolicy);

/** SMTP username */
const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName });

/** Custom resource handler to generate a SMTP password */
const passwordFunction = new NodejsFunction(this, 'PasswordFunction', {
description: 'Supabase - Generate SMTP Password Function',
entry: path.resolve(__dirname, 'cr-smtp-password.ts'),
runtime: lambda.Runtime.NODEJS_18_X,
});

/** Custom resource provider to generate a SMTP password */
const passwordProvider = new cr.Provider(this, 'PasswordProvider', { onEventHandler: passwordFunction });

/** SMTP password */
const password = new cdk.CustomResource(this, 'Password', {
resourceType: 'Custom::Password',
serviceToken: passwordProvider.serviceToken,
properties: {
Region: region,
SecretAccessKey: accessKey.attrSecretAccessKey,
},
});

const stackId = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Aws.STACK_ID));

/** Amazon WorkMail Stack */
const workMail = new WorkMailStack(this, 'WorkMail', {
description: 'Amazon WorkMail for Test Domain',
organization: { region: region, alias: stackId },
});
// Add condition
(workMail.node.defaultChild as cdk.CfnStack).cfnOptions.condition = workMailEnabled;

/** The mail user on WorkMail */
const workMailUser = workMail.organization.addUser('Supabase', password.getAttString('Password'));

this.host = cdk.Fn.conditionIf(workMailEnabled.logicalId, `smtp.mail.${region}.awsapps.com`, `email-smtp.${region}.amazonaws.com`).toString();
this.port = 465;
this.email = cdk.Fn.conditionIf(workMailEnabled.logicalId, workMailUser.getAtt('Email'), email).toString();

/**
* SMTP username
*
* If WorkMail is enabled, use the WorkMail user's email address.
*/
const username = cdk.Fn.conditionIf(workMailEnabled.logicalId, workMailUser.getAtt('Email'), accessKey.ref).toString();

this.secret = new Secret(this, 'Secret', {
secretName: `${cdk.Aws.STACK_NAME}${id}Secret`,
description: 'Supabase - SMTP Secret',
secretObjectValue: {
username: cdk.SecretValue.unsafePlainText(username),
password: cdk.SecretValue.resourceAttribute(password.getAttString('Password')),
host: cdk.SecretValue.unsafePlainText(this.host),
},
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Payload extends Object {
role: string;
}

/** Get the JWT secret */
const getJwtSecret = async (secretId: string) => {
const client = new SecretsManagerClient({ region });
const cmd = new GetSecretValueCommand({ SecretId: secretId });
Expand All @@ -18,6 +19,7 @@ const getJwtSecret = async (secretId: string) => {
return SecretString!;
};

/** Generate a json web token */
const generateToken = async (payload: object, secretId: string, issuer?: string, expiresIn?: string) => {
const jwtSecret = await getJwtSecret(secretId);
const token = jwt.sign(payload, jwtSecret, { issuer, expiresIn });
Expand Down
26 changes: 20 additions & 6 deletions src/json-web-token.ts → src/json-web-token/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
Expand All @@ -7,8 +8,10 @@ import * as cr from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';

export class JwtSecret extends Secret {
/** Custom resource provider to generate a json web token */
genTokenProvider: cr.Provider;

/** Creates a new jwt secret in AWS SecretsManager. */
constructor(scope: Construct, id: string, props?: SecretProps) {
super(scope, id, {
description: `${cdk.Aws.STACK_NAME} - Json Web Token Secret`,
Expand All @@ -19,17 +22,23 @@ export class JwtSecret extends Secret {
...props,
});

const genTokenFunction = new NodejsFunction(this, 'GenerateTokenFunction', {
/** Custom resource handler to generate a json web token */
const jwtFunction = new NodejsFunction(this, 'JsonWebTokenFunction', {
description: `${cdk.Aws.STACK_NAME} - Generate token via jwt secret`,
entry: './src/functions/gen-json-web-token.ts',
entry: path.resolve(__dirname, 'cr-json-web-token.ts'),
runtime: lambda.Runtime.NODEJS_18_X,
environment: { JWT_SECRET_ARN: this.secretArn },
environment: {
JWT_SECRET_ARN: this.secretArn,
},
});
this.grantRead(genTokenFunction);

this.genTokenProvider = new cr.Provider(this, 'GenerateTokenProvider', { onEventHandler: genTokenFunction });
/** Allow the function to read the jwt secret */
this.grantRead(jwtFunction);

this.genTokenProvider = new cr.Provider(this, 'GenerateTokenProvider', { onEventHandler: jwtFunction });
}

/** Generate a new token in ParameterStore. */
genApiKey(id: string, props: ApiKeyProps) {
const apiKey = new ApiKey(this, id, props);
return apiKey;
Expand All @@ -43,9 +52,12 @@ interface ApiKeyProps {
}

class ApiKey extends Construct {
/** Token value */
value: string;
/** ParameterStore of the token */
ssmParameter: ssm.StringParameter;

/** Json Web Token */
constructor(scope: JwtSecret, id: string, props: ApiKeyProps) {
super(scope, id);

Expand All @@ -54,7 +66,8 @@ class ApiKey extends Construct {
const issuer = props.issuer;
const expiresIn = props.expiresIn;

const token = new cdk.CustomResource(this, 'Token', {
/** String value of Json Web Token */
const token = new cdk.CustomResource(this, 'Resource', {
serviceToken: jwtSecret.genTokenProvider.serviceToken,
resourceType: 'Custom::JsonWebToken',
properties: {
Expand All @@ -63,6 +76,7 @@ class ApiKey extends Construct {
ExpiresIn: expiresIn,
},
});

this.value = token.getAttString('Value');

this.ssmParameter = new ssm.StringParameter(this, 'Parameter', {
Expand Down
3 changes: 2 additions & 1 deletion src/supabase-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ export class SupabaseDatabase extends Construct {
});

this.cluster = new rds.DatabaseCluster(this, 'Cluster', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
engine,
parameterGroup,
vpc,
writer: rds.ClusterInstance.serverlessV2('Instance1'),
readers: [
rds.ClusterInstance.serverlessV2('Instance2', { scaleWithWriter: true }),
],
vpc,
credentials: rds.Credentials.fromGeneratedSecret('supabase_admin', {
secretName: `${cdk.Aws.STACK_NAME}-${id}-supabase_admin`,
}),
Expand Down
Loading

0 comments on commit 27e7c3e

Please sign in to comment.