From 176be2a680c802d9ec893f69a417d7745f0b420e Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 25 Aug 2023 16:55:11 +0200 Subject: [PATCH 1/9] feat: Make naamIngelogdeGebruiker optional Add better test for anonymous submissions. --- src/app/submission/Submission.ts | 2 +- src/app/submission/SubmissionSchema.ts | 4 +-- .../test/samples/sns.sample-anonymous.json | 34 +++++++++++++++++++ src/app/submission/test/submission.test.ts | 8 +++-- 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/app/submission/test/samples/sns.sample-anonymous.json diff --git a/src/app/submission/Submission.ts b/src/app/submission/Submission.ts index 9de21600..c2ac33aa 100644 --- a/src/app/submission/Submission.ts +++ b/src/app/submission/Submission.ts @@ -39,7 +39,7 @@ export class Submission { async parse(message: any) { this.rawSubmission = message; - const contents = JSON.parse(message.Message); + const contents = JSON.parse(message.Message); this.parsedSubmission = SubmissionSchema.passthrough().parse(contents); this.bsn = this.parsedSubmission.bsn; this.kvk = this.parsedSubmission.kvk; diff --git a/src/app/submission/SubmissionSchema.ts b/src/app/submission/SubmissionSchema.ts index 41286ae8..773127cf 100644 --- a/src/app/submission/SubmissionSchema.ts +++ b/src/app/submission/SubmissionSchema.ts @@ -12,8 +12,8 @@ export const SubmissionSchema = z.object({ reference: z.string(), data: z.object({ kenmerk: z.string(), - naamIngelogdeGebruiker: z.string(), - }).required().passthrough(), + naamIngelogdeGebruiker: z.string().optional(), + }).passthrough(), employeeData: z.any(), pdf: z.object({ reference: z.string(), diff --git a/src/app/submission/test/samples/sns.sample-anonymous.json b/src/app/submission/test/samples/sns.sample-anonymous.json new file mode 100644 index 00000000..fa872f3a --- /dev/null +++ b/src/app/submission/test/samples/sns.sample-anonymous.json @@ -0,0 +1,34 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-west-1: 315037222840:eform-submissions:293455ad-bb4b-4667-971d-7eaba358510d", + "Sns": { + "Type": "Notification", + "MessageId": "538c926d-27a7-567b-b314-cf7e54168743", + "TopicArn": "arn:aws:sns:eu-west-1:315037222840:eform-submissions", + "Message": "{\"formId\": \"82307c98-b8fa-4a31-825d-730496dd1e50\",\"formTypeId\": \"formnaam\",\"appId\": \"TDL\",\"reference\": \"TDL123.000\",\"data\": {\"telefoonnummer\": \"\",\"eMailadres\": \"me@example.com\",\"isIemandAndersUwContactpersoon\": \"nee\",\"kenmerk\": \"TDL123.000\",\"volgende\": true,\"waaroverHeeftUEenVraag\": {\"ikHebHulpNodigBijHetHuishouden\": true,\"ikHebHulpOfEenHulpmiddelNodigBijHetVervoer\": false,\"ikHebEenHulpmiddelOfAanpassingInMijnWoningNodig\": false,\"anders\": false},\"watIsUwVraag\": \"Graag hulp bij het huishouden.\",\"vorige1\": false,\"volgende1\": true,\"toevoegen\": [],\"vorige2\": false,\"volgende2\": true,\"summary1\": {},\"ikHebAlleVragenNaarWaarheidBeantwoord\": true,\"vorige3\": false,\"verzenden2\": true},\"metadata\": {\"timezone\": \"Europe/Amsterdam\",\"offset\": 120,\"origin\": \"https://app6-accp.nijmegen.nl\",\"referrer\": \"https://www.nijmegen.nl/\",\"browserName\": \"Netscape\",\"userAgent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\",\"pathName\": \"/\",\"onLine\": true,\"timestamp\": [2023,8,25,12,20,47,238191797]},\"pdf\": {\"reference\": \"blaat\",\"location\": \"/pdf/TDL.1230\",\"bucketName\": \"mybucket\"}}", + "Timestamp": "2023-06-26T11:07:29.154Z", + "SignatureVersion": "1", + "Signature": "MP6SMqgHFUA3eWuR/PkRLKW/eLzsbGa81a7NVN7/8Fq94K9X9DgaiqMmWq1CwWNpR5K8ISd5lwE+MIBK1h19lFVhYa/QgbIWsys8G9pCvfHdI1s+xzEAfVxgrRPa71QHkGNrrxqJN5c78ahChN9IG/XRPddYqltei5ZX6BRdFwTc0pGLkCy0hNj6kqJ30VGT8jpJoo/+PFt0PXTs+NCaOSfUo+kxO2VKtgXmHElPzfb7VAV2QFGtVkcGBgnxA5o0eEwepjf844L/BC3YsHxqHLVeg9rreMtkvoAsETRo9Qy8anYC1TcQ4rkmyWaKp4QlSllidzZxjKtbgvuC5ZPHRQ==", + "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-01d088a6f77103d0fe307c0069e40ed6.pem", + "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:315037222840:eform-submissions:bb9d78bf-834b-4146-898c-ff3456b26f9e", + "MessageAttributes": { + "BRP_data": { + "Type": "String", + "Value": "true" + }, + "AppId": { + "Type": "String", + "Value": "APV" + }, + "KVK_data": { + "Type": "String", + "Value": "false" + } + } + } + } + ] +} diff --git a/src/app/submission/test/submission.test.ts b/src/app/submission/test/submission.test.ts index 8f8bb181..fc6ba5e2 100644 --- a/src/app/submission/test/submission.test.ts +++ b/src/app/submission/test/submission.test.ts @@ -1,4 +1,5 @@ import * as snsSample from './samples/sns.sample.json'; +import * as snsSampleAnonymous from './samples/sns.sample-anonymous.json'; import { MockDatabase } from '../Database'; import { MockFormConnector } from '../FormConnector'; import { MockStorage } from '../Storage'; @@ -25,10 +26,11 @@ describe('Submission parsing', () => { }); test('Message without kvk or bsn key is anonymous', async () => { - const anonMessage = JSON.parse(JSON.stringify(message)); - anonMessage.Message = anonMessage.Message.replace(',\"bsn\":\"900222670\"', ''); + const messagesAnonymous = snsSampleAnonymous.Records.map(record => record.Sns); + const messageAnonymous = messagesAnonymous.pop(); + const anonMessage = JSON.parse(JSON.stringify(messageAnonymous)); const anonSubmission = new Submission({ storage, formConnector, database }); - await submission.parse(anonMessage); + await anonSubmission.parse(anonMessage); expect(anonSubmission.isAnonymous()).toBe(true); }); From a361234c879bdbcde1e01e798a4b8400c893e94a Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 8 Sep 2023 09:08:06 +0200 Subject: [PATCH 2/9] docs: Add readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3fa7ddc..baad030f 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# replace this \ No newline at end of file +# Webformulieren submission storage + +Deze applicatie is verantwoordelijk voor de opslag van formulierinzendingen, voor afnemers (Mijn Nijmegen) die deze moeten kunnen tonen aan de indiener. De applicatie is gesubscribed op het SNS-topic dat in [webformulieren](https://github.com/gemeentenijmegen/webformulieren) bestaat, en verwerkt inzendingen bij binnenkomst. + +Het is nog niet mogelijk ingezonden formulieren op te halen. From f7fbaf94c0ccd0c1f21c1d4e0213672c97b74e01 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 8 Sep 2023 07:17:44 +0000 Subject: [PATCH 3/9] chore: self mutation Signed-off-by: github-actions --- src/app/submission/Submission.ts | 2 +- src/app/submission/test/submission.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/submission/Submission.ts b/src/app/submission/Submission.ts index c2ac33aa..9de21600 100644 --- a/src/app/submission/Submission.ts +++ b/src/app/submission/Submission.ts @@ -39,7 +39,7 @@ export class Submission { async parse(message: any) { this.rawSubmission = message; - const contents = JSON.parse(message.Message); + const contents = JSON.parse(message.Message); this.parsedSubmission = SubmissionSchema.passthrough().parse(contents); this.bsn = this.parsedSubmission.bsn; this.kvk = this.parsedSubmission.kvk; diff --git a/src/app/submission/test/submission.test.ts b/src/app/submission/test/submission.test.ts index fc6ba5e2..022ef348 100644 --- a/src/app/submission/test/submission.test.ts +++ b/src/app/submission/test/submission.test.ts @@ -1,5 +1,5 @@ -import * as snsSample from './samples/sns.sample.json'; import * as snsSampleAnonymous from './samples/sns.sample-anonymous.json'; +import * as snsSample from './samples/sns.sample.json'; import { MockDatabase } from '../Database'; import { MockFormConnector } from '../FormConnector'; import { MockStorage } from '../Storage'; From 050b66057c75161e4e063472caa8238ed982d680 Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Tue, 24 Oct 2023 13:34:10 +0200 Subject: [PATCH 4/9] feat: Create SNS topic for cross account posting (#17) Adds an SNS topic, which can be subscribed to from a different account. It allows the accp webformulieren account to post to this topic. Submissions on this topic will be processed the same way other submissions are handled. --- src/ApiStack.ts | 93 +++++++++++++++---- src/ApiStage.ts | 8 +- src/Configuration.ts | 8 ++ src/SubmissionSnsEventHandler.ts | 15 ++- .../samples/sns.sample-duplicate-files.json | 34 +++++++ src/statics.ts | 2 + test/main.test.ts | 35 ++++--- 7 files changed, 154 insertions(+), 41 deletions(-) create mode 100644 src/app/submission/test/samples/sns.sample-duplicate-files.json diff --git a/src/ApiStack.ts b/src/ApiStack.ts index f234e3d8..fa95f716 100644 --- a/src/ApiStack.ts +++ b/src/ApiStack.ts @@ -1,33 +1,90 @@ -import { Stack } from 'aws-cdk-lib'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { AnyPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { ITopic, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; +import { Configurable } from './Configuration'; import { Statics } from './statics'; import { SubmissionSnsEventHandler } from './SubmissionSnsEventHandler'; +interface ApiStackProps extends StackProps, Configurable {}; /** * Contains all API-related resources. */ export class ApiStack extends Stack { - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: ApiStackProps) { + super(scope, id, props); + + const internalTopic = new SNSTopic(this, 'submissions', { publishingAccountIds: props.configuration.allowedAccountIdsToPublishToSNS }); + new SubmissionSnsEventHandler(this, 'submissionhandler', { + topicArns: [internalTopic.topic.topicArn, StringParameter.valueForStringParameter(this, Statics.ssmSubmissionTopicArn)], + }); + } +} + +interface SNSTopicProps extends StackProps { + /** + * Should the topic be protected by the KMS key (default true) + */ + keyProtected?: boolean; + + /** + * Allow access for different AWS accounts to publish to this topic + */ + publishingAccountIds?: string[]; +} +class SNSTopic extends Construct { + topic: ITopic; + constructor(scope: Construct, id: string, props: SNSTopicProps) { super(scope, id); - // const api = new RestApi(this, 'gateway'); - // api.root.addMethod('ANY', new MockIntegration({ - // integrationResponses: [ - // { statusCode: '200' }, - // ], - // passthroughBehavior: PassthroughBehavior.NEVER, - // requestTemplates: { - // 'application/json': '{ "statusCode": 200 }', - // }, - // }), { - // methodResponses: [ - // { statusCode: '200' }, - // ], - // }); + let masterKey = (props.keyProtected !== false) ? this.encryptionKey() : undefined; - new SubmissionSnsEventHandler(this, 'submissionhandler', { - topicArn: StringParameter.valueForStringParameter(this, Statics.ssmSubmissionTopicArn), + this.topic = new Topic(this, 'submissions', { + displayName: 'submissions', + masterKey, }); + + this.allowCrossAccountAccess(props.publishingAccountIds); + } + + /** + * Returns the customer manager key for the project + * + * @returns the encryption key + */ + private encryptionKey() { + let masterKey = undefined; + masterKey = Key.fromKeyArn(this, 'key', StringParameter.valueForStringParameter(this, Statics.ssmDataKeyArn)); + return masterKey; + } + + /** + * Allow cross account access to this topic + * + * This allows lambda's with the execution role 'storesubmissions-lambda-role' + * in the accounts in `allowedAccountIds` access to publish to this topic. + * + * @param allowedAccountIds array of account IDs + */ + allowCrossAccountAccess(allowedAccountIds?: string[]): void { + if (!allowedAccountIds || allowedAccountIds.length == 0) { return; } + const crossAccountPrincipalArns = allowedAccountIds.map( + (accountId) => `arn:aws:iam::${accountId}:role/storesubmissions-lambda-role`, + ); + this.topic.addToResourcePolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'SNS:Publish', + ], + resources: [this.topic.topicArn], + principals: [new AnyPrincipal()], + conditions: { + ArnLike: { + 'aws:PrincipalArn': crossAccountPrincipalArns, + }, + }, + })); } } diff --git a/src/ApiStage.ts b/src/ApiStage.ts index d0708c64..86e342cc 100644 --- a/src/ApiStage.ts +++ b/src/ApiStage.ts @@ -2,7 +2,7 @@ import { PermissionsBoundaryAspect } from '@gemeentenijmegen/aws-constructs'; import { Aspects, Stage, StageProps, Tags } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { ApiStack } from './ApiStack'; -import { Configurable, Configuration } from './Configuration'; +import { Configurable } from './Configuration'; import { Statics } from './statics'; import { StorageStack } from './StorageStack'; @@ -13,8 +13,6 @@ interface ApiStageProps extends StageProps, Configurable { } */ export class ApiStage extends Stage { - readonly configuration: Configuration; - constructor(scope: Construct, id: string, props: ApiStageProps) { super(scope, id, props); @@ -22,11 +20,9 @@ export class ApiStage extends Stage { Tags.of(this).add('Project', Statics.projectName); Aspects.of(this).add(new PermissionsBoundaryAspect()); - this.configuration = props.configuration; - const storageStack = new StorageStack(this, 'storage'); - const apiStack = new ApiStack(this, 'api'); + const apiStack = new ApiStack(this, 'api', { configuration: props.configuration } ); apiStack.addDependency(storageStack); } } diff --git a/src/Configuration.ts b/src/Configuration.ts index 191651d5..2593be66 100644 --- a/src/Configuration.ts +++ b/src/Configuration.ts @@ -26,6 +26,13 @@ export interface Configuration { * includePipelineValidationChcks */ readonly includePipelineValidationChecks: boolean; + + /** + * Allow this project's SNS topic to be published to + * by other accounts. This allows access to lambda + * execution roles named 'storesubmissions-lambda-role'. + */ + readonly allowedAccountIdsToPublishToSNS?: string[]; } export function getConfiguration(branchName: string): Configuration { @@ -45,6 +52,7 @@ const configurations: { [name: string] : Configuration } = { deployFromEnvironment: Statics.gnBuildEnvironment, deployToEnvironment: Statics.appDevEnvironment, includePipelineValidationChecks: false, + allowedAccountIdsToPublishToSNS: [Statics.acceptanceWebformulierenAccountId], }, production: { branchName: 'main', diff --git a/src/SubmissionSnsEventHandler.ts b/src/SubmissionSnsEventHandler.ts index 644f4abc..2a541753 100644 --- a/src/SubmissionSnsEventHandler.ts +++ b/src/SubmissionSnsEventHandler.ts @@ -2,6 +2,7 @@ import { Duration } from 'aws-cdk-lib'; import { ITable, Table } from 'aws-cdk-lib/aws-dynamodb'; import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; +import { Function } from 'aws-cdk-lib/aws-lambda'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3'; import { ISecret, Secret } from 'aws-cdk-lib/aws-secretsmanager'; @@ -13,14 +14,14 @@ import { SubmissionFunction } from './app/submission/submission-function'; import { Statics } from './statics'; interface SubmissionSnsEventHandlerProps { - topicArn: string; + topicArns: string[]; } export class SubmissionSnsEventHandler extends Construct { private role?: Role; + public lambda: Function; constructor(scope: Construct, id: string, props: SubmissionSnsEventHandlerProps) { super(scope, id); - const topic = Topic.fromTopicArn(this, 'submission-topic', props.topicArn); const table = Table.fromTableName(this, 'table', StringParameter.valueForStringParameter(this, Statics.ssmSubmissionTableName)); const key = Key.fromKeyArn(this, 'key', StringParameter.valueForStringParameter(this, Statics.ssmDataKeyArn)); // IBucket requires encryption key, otherwise grant methods won't add the correct permissions @@ -31,7 +32,8 @@ export class SubmissionSnsEventHandler extends Construct { const sourceBucket = Bucket.fromBucketArn(this, 'sourceBucket', StringParameter.valueForStringParameter(this, Statics.ssmSourceBucketArn)); const secret = Secret.fromSecretNameV2(this, 'apikey', Statics.secretFormIoApiKey); - this.submissionHandlerLambda(storageBucket, sourceBucket, table, topic, secret); + const topics = props.topicArns.map((topicArn, i)=> Topic.fromTopicArn(this, `submission-topic-${i}`, topicArn)); + this.lambda = this.submissionHandlerLambda(storageBucket, sourceBucket, table, topics, secret); } /** @@ -46,7 +48,7 @@ export class SubmissionSnsEventHandler extends Construct { * @param table The dynamodb table to store submission (meta)data in * @param topic The SNS Topic to subscribe to for submissions */ - private submissionHandlerLambda(bucket: IBucket, sourceBucket: IBucket, table: ITable, topic: ITopic, secret: ISecret) { + private submissionHandlerLambda(bucket: IBucket, sourceBucket: IBucket, table: ITable, topics: ITopic[], secret: ISecret) { const submissionLambda = new SubmissionFunction(this, 'submission', { role: this.lambdaRole(), logRetention: RetentionDays.SIX_MONTHS, @@ -65,7 +67,10 @@ export class SubmissionSnsEventHandler extends Construct { const key = Key.fromKeyArn(this, 'sourceBucketKey', StringParameter.valueForStringParameter(this, Statics.ssmSourceKeyArn)); key.grantDecrypt(submissionLambda); - topic.addSubscription(new LambdaSubscription(submissionLambda)); + for (const topic of topics) { + topic.addSubscription(new LambdaSubscription(submissionLambda)); + } + return submissionLambda; } /** diff --git a/src/app/submission/test/samples/sns.sample-duplicate-files.json b/src/app/submission/test/samples/sns.sample-duplicate-files.json new file mode 100644 index 00000000..81c17568 --- /dev/null +++ b/src/app/submission/test/samples/sns.sample-duplicate-files.json @@ -0,0 +1,34 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-west-1: 315037222840:eform-submissions:293455ad-bb4b-4667-971d-7eaba358510d", + "Sns": { + "Type": "Notification", + "MessageId": "538c926d-27a7-567b-b314-cf7e54168743", + "TopicArn": "arn:aws:sns:eu-west-1:315037222840:eform-submissions", + "Message": "{\"formId\": \"68c11b91-d127-414f-89cc-c6ef7ab3e1bf\",\"formTypeId\": \"test\",\"appId\": \"TDL\",\"reference\": \"TDL17.957\",\"data\": {\"bsn\": \"900026236\",\"kvk\": \"\",\"achternaam\": \"\",\"betrouwbaarheidsniveau\": \"digid\",\"familienaam\": \"\",\"geboortedatum\": \"01-01-1956\",\"gemeente\": \"\",\"geslacht\": \"\",\"huisnummer\": \"\",\"initialen\": \"\",\"inlogmiddel\": \"digid\",\"kenmerk\": \"TDL-17.957\",\"naamIngelogdeGebruiker\": \"H. de Jong\",\"nationaliteit\": \"\",\"postcode\": \"\",\"straatnaam\": \"\",\"totaalbedrag\": \"\",\"tussenvoegsel\": \"\",\"vipZaakType\": \"\",\"volledigeNaam\": \"H. de Jong\",\"voornaam\": \"\",\"woonplaats\": \"Nijmegen\",\"adres_nijmegen\": {\"adres_nijmegen-postalcode\": \"6511 pp\",\"adres_nijmegen-housenr\": \"6\",\"adres_nijmegen-streetname\": \"Korte Nieuwstraat\",\"adres_nijmegen-city\": \"Nijmegen\"},\"select_nijmegen\": \"\",\"eMailadres\": \"\",\"selectBoxesNijmegen\": {\"test\": true,\"tweede\": false,\"derde\": false},\"test\": false,\"dataGrid\": [{\"textareaNijmegen\": \"\"}],\"fileNijmegen\": [{\"storage\": \"url\",\"name\": \"test-1m-14cfa303-e58a-42dc-bd47-1299a789a627.pdf\",\"url\": \"https://example.com/-test-1m-blaat.pdf\",\"size\": \"1048576\",\"type\": \"application/pdf\",\"reference\": \"360fa9d4e798e8902d19e9757eef63ad\",\"originalName\": \"test-1m-blaat.pdf\",\"location\": \"randomid/files/test-1m-blaat.pdf\",\"bucketName\": \"randombucket\"},{\"storage\": \"url\",\"name\": \"test-1m-1differentrandomid.pdf\",\"url\": \"https://example.com/-test-1m-blaat.pdf\",\"size\": \"1048576\",\"type\": \"application/pdf\",\"reference\": \"360fa9d4e798e8902d19e9757eef63ad\",\"originalName\": \"test-1m-blaat.pdf\",\"location\": \"randomdifferentid/files/test-1m-blaat.pdf\",\"bucketName\": \"randombucket\"},{\"storage\": \"url\",\"name\": \"test-1m-14cfa303-e58a-42dc-bd47-1299a789a627.pdf\",\"url\": \"https://example.com/-1299a789a627.pdf\",\"size\": \"1048576\",\"type\": \"application/pdf\",\"reference\": \"360fa9d4e798e8902d19e9757eef63ad\",\"originalName\": \"test-1m.pdf\",\"location\": \"randomid/files/test-1m.pdf\",\"bucketName\": \"randombucket\"}],\"opslaan\": false,\"volgende\": true,\"submit1\": false,\"submit\": true},\"metadata\": {\"timezone\": \"Europe/Amsterdam\",\"offset\": 120,\"origin\": \"https://app6-accp.nijmegen.nl\",\"referrer\": \"\",\"browserName\": \"Netscape\",\"userAgent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.15\",\"pathName\": \"/\",\"onLine\": true},\"employeeData\": {},\"pdf\": {\"reference\": \"c07e4f3b36955bb31565fb1ef7bdefaf\",\"location\": \"randomid/pdf/TDL17.957\",\"bucketName\": \"randombucket\"},\"brpData\": {\"Persoon\": {\"BSN\": {\"BSN\": \"900026236\"},\"Persoonsgegevens\": {\"Voorletters\": \"H.\",\"Voornamen\": \"Hans\",\"Voorvoegsel\": \"de\",\"Geslachtsnaam\": \"Jong\",\"Achternaam\": \"de Jong\",\"Naam\": \"H. de Jong\",\"Geboortedatum\": \"01-01-1956\",\"Geslacht\": \"M\",\"NederlandseNationaliteit\": \"Ja\",\"Geboorteplaats\": \"Nijmegen\",\"Geboorteland\": \"Nederland\"},\"Adres\": {\"Straat\": \"Kelfkensbos\",\"Huisnummer\": \"80-A-1\",\"Gemeente\": \"Nijmegen\",\"Postcode\": \"6511 RN\",\"Woonplaats\": \"Nijmegen\"},\"Reisdocument\": {\"Documentsoort\": \"\",\"Documentnummer\": \"\",\"Uitgiftedatum\": \"\",\"Verloopdatum\": \"\"},\"ageLimits\": {\"over12\": \"Yes\",\"over16\": \"Yes\",\"over18\": \"Yes\",\"over21\": \"Yes\",\"over65\": \"Yes\"}}},\"bsn\": \"900026236\"}", + "Timestamp": "2023-06-26T11:07:29.154Z", + "SignatureVersion": "1", + "Signature": "MP6SMqgHFUA3eWuR/PkRLKW/eLzsbGa81a7NVN7/8Fq94K9X9DgaiqMmWq1CwWNpR5K8ISd5lwE+MIBK1h19lFVhYa/QgbIWsys8G9pCvfHdI1s+xzEAfVxgrRPa71QHkGNrrxqJN5c78ahChN9IG/XRPddYqltei5ZX6BRdFwTc0pGLkCy0hNj6kqJ30VGT8jpJoo/+PFt0PXTs+NCaOSfUo+kxO2VKtgXmHElPzfb7VAV2QFGtVkcGBgnxA5o0eEwepjf844L/BC3YsHxqHLVeg9rreMtkvoAsETRo9Qy8anYC1TcQ4rkmyWaKp4QlSllidzZxjKtbgvuC5ZPHRQ==", + "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-01d088a6f77103d0fe307c0069e40ed6.pem", + "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:315037222840:eform-submissions:bb9d78bf-834b-4146-898c-ff3456b26f9e", + "MessageAttributes": { + "BRP_data": { + "Type": "String", + "Value": "true" + }, + "AppId": { + "Type": "String", + "Value": "APV" + }, + "KVK_data": { + "Type": "String", + "Value": "false" + } + } + } + } + ] +} diff --git a/src/statics.ts b/src/statics.ts index 85a6349f..5d284b59 100644 --- a/src/statics.ts +++ b/src/statics.ts @@ -21,6 +21,8 @@ export abstract class Statics { region: 'eu-central-1', }; + static readonly acceptanceWebformulierenAccountId = '315037222840'; + static ssmDataKeyArn: string = `/${this.projectName}/dataKeyArn`; static ssmSubmissionBucketArn: string = `/${this.projectName}/submissionBucketArn`; static ssmSourceBucketArn: string = `/${this.projectName}/sourceBucketArn`; diff --git a/test/main.test.ts b/test/main.test.ts index 678d8773..7fe13bf8 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -1,24 +1,35 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; +import { ApiStack } from '../src/ApiStack'; import { Configuration } from '../src/Configuration'; import { PipelineStack } from '../src/PipelineStack'; +const configuration: Configuration = { + branchName: 'stacktest', + deployFromEnvironment: { + account: '12345678', + region: 'eu-central-1', + }, + deployToEnvironment: { + account: '12345678', + region: 'eu-central-1', + }, + includePipelineValidationChecks: false, +}; + test('Snapshot', () => { const app = new App(); - const configuration: Configuration = { - branchName: 'stacktest', - deployFromEnvironment: { - account: '12345678', - region: 'eu-central-1', - }, - deployToEnvironment: { - account: '12345678', - region: 'eu-central-1', - }, - includePipelineValidationChecks: false, - }; + const stack = new PipelineStack(app, 'test', { env: configuration.deployFromEnvironment, configuration }); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); }); + + +test('Api Stack', () => { + const app = new App(); + const apiStack = new ApiStack(app, 'api', { configuration }); + const template = Template.fromStack(apiStack); + expect(template.resourceCountIs('AWS::SNS::Subscription', 2)); +}); From dca4c41782e43c79458bd60a6c927dfe107f063a Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 27 Oct 2023 14:32:08 +0200 Subject: [PATCH 5/9] fix: Allow KMS access for lambda role (#18) Allows the lambda role to publish to the encrypted SNS topic. --- src/ApiStage.ts | 6 ++++-- src/StorageStack.ts | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/ApiStage.ts b/src/ApiStage.ts index 86e342cc..3cf453eb 100644 --- a/src/ApiStage.ts +++ b/src/ApiStage.ts @@ -20,9 +20,11 @@ export class ApiStage extends Stage { Tags.of(this).add('Project', Statics.projectName); Aspects.of(this).add(new PermissionsBoundaryAspect()); - const storageStack = new StorageStack(this, 'storage'); + const configuration = props.configuration; - const apiStack = new ApiStack(this, 'api', { configuration: props.configuration } ); + const storageStack = new StorageStack(this, 'storage', { configuration }); + + const apiStack = new ApiStack(this, 'api', { configuration } ); apiStack.addDependency(storageStack); } } diff --git a/src/StorageStack.ts b/src/StorageStack.ts index b949d4c7..e4da2d93 100644 --- a/src/StorageStack.ts +++ b/src/StorageStack.ts @@ -1,18 +1,23 @@ -import { Duration, Stack } from 'aws-cdk-lib'; +import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import { AttributeType, BillingMode, Table, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; import { Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; +import { Configurable } from './Configuration'; import { Statics } from './statics'; + +interface StorageStackProps extends StackProps, Configurable {}; + /** * Contains all API-related resources. */ export class StorageStack extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); + constructor(scope: Construct, id: string, props: StorageStackProps) { + super(scope, id, props); const key = this.key(); /** @@ -48,13 +53,16 @@ export class StorageStack extends Stack { this.addParameters(); } - private key() { + private key(crossAccountIds?: string[]) { + const crossAccountPrincipalArns = this.crossAccountIdArns(crossAccountIds); const key = new Key(this, 'kmskey', { enableKeyRotation: true, description: 'encryption key for user data', alias: `${Statics.projectName}/user-data`, }); + this.allowCrossAccountKeyAccess(crossAccountPrincipalArns, key); + // Store key arn to be used in other stacks/projects new StringParameter(this, 'key', { stringValue: key.keyArn, @@ -64,6 +72,33 @@ export class StorageStack extends Stack { return key; } + private allowCrossAccountKeyAccess(crossAccountPrincipalArns: string[] | null, key: Key) { + if (crossAccountPrincipalArns) { + key.addToResourcePolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'kms:GenerateDataKey', + 'kms:Decrypt', + ], + resources: [key.keyArn], + conditions: { + ArnLike: { + 'aws:PrincipalArn': crossAccountPrincipalArns, + }, + }, + }), false); + } + } + + private crossAccountIdArns(crossAccountIds: string[] | undefined) { + if (crossAccountIds && crossAccountIds.length > 0) { + return crossAccountIds.map( + (accountId) => `arn:aws:iam::${accountId}:role/storesubmissions-lambda-role`, + ); + } + return null; + } + private addArnToParameterStore(id: string, arn: string, name: string) { new StringParameter(this, id, { stringValue: arn, From e43588345743d9a753353c4b628f541be9c19c5f Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 27 Oct 2023 14:37:28 +0200 Subject: [PATCH 6/9] fix: Allow KMS access for lambda role (#19) Allows the lambda role to publish to the encrypted SNS topic. From cb90c1bd9bc78bf1135ff34672eff98f1064ad2d Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 27 Oct 2023 15:59:00 +0200 Subject: [PATCH 7/9] Fix/allow key access (#20) * fix: Allow KMS access for lambda role Allows the lambda role to publish to the encrypted SNS topic. * fix: remove kms references, topic Cross-account-cross-region KMS access with existing key not possible. * fix unused referenes * remove last unused reference --- src/ApiStack.ts | 15 ---- src/StorageStack.ts | 34 +-------- src/StorageStack.ts.orig | 144 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 src/StorageStack.ts.orig diff --git a/src/ApiStack.ts b/src/ApiStack.ts index fa95f716..ded3cb0e 100644 --- a/src/ApiStack.ts +++ b/src/ApiStack.ts @@ -1,6 +1,5 @@ import { Stack, StackProps } from 'aws-cdk-lib'; import { AnyPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; -import { Key } from 'aws-cdk-lib/aws-kms'; import { ITopic, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; @@ -39,27 +38,13 @@ class SNSTopic extends Construct { constructor(scope: Construct, id: string, props: SNSTopicProps) { super(scope, id); - let masterKey = (props.keyProtected !== false) ? this.encryptionKey() : undefined; - this.topic = new Topic(this, 'submissions', { displayName: 'submissions', - masterKey, }); this.allowCrossAccountAccess(props.publishingAccountIds); } - /** - * Returns the customer manager key for the project - * - * @returns the encryption key - */ - private encryptionKey() { - let masterKey = undefined; - masterKey = Key.fromKeyArn(this, 'key', StringParameter.valueForStringParameter(this, Statics.ssmDataKeyArn)); - return masterKey; - } - /** * Allow cross account access to this topic * diff --git a/src/StorageStack.ts b/src/StorageStack.ts index e4da2d93..669fb12b 100644 --- a/src/StorageStack.ts +++ b/src/StorageStack.ts @@ -1,6 +1,5 @@ import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import { AttributeType, BillingMode, Table, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; -import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; import { Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; @@ -53,16 +52,13 @@ export class StorageStack extends Stack { this.addParameters(); } - private key(crossAccountIds?: string[]) { - const crossAccountPrincipalArns = this.crossAccountIdArns(crossAccountIds); + private key() { const key = new Key(this, 'kmskey', { enableKeyRotation: true, description: 'encryption key for user data', alias: `${Statics.projectName}/user-data`, }); - this.allowCrossAccountKeyAccess(crossAccountPrincipalArns, key); - // Store key arn to be used in other stacks/projects new StringParameter(this, 'key', { stringValue: key.keyArn, @@ -71,34 +67,6 @@ export class StorageStack extends Stack { return key; } - - private allowCrossAccountKeyAccess(crossAccountPrincipalArns: string[] | null, key: Key) { - if (crossAccountPrincipalArns) { - key.addToResourcePolicy(new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'kms:GenerateDataKey', - 'kms:Decrypt', - ], - resources: [key.keyArn], - conditions: { - ArnLike: { - 'aws:PrincipalArn': crossAccountPrincipalArns, - }, - }, - }), false); - } - } - - private crossAccountIdArns(crossAccountIds: string[] | undefined) { - if (crossAccountIds && crossAccountIds.length > 0) { - return crossAccountIds.map( - (accountId) => `arn:aws:iam::${accountId}:role/storesubmissions-lambda-role`, - ); - } - return null; - } - private addArnToParameterStore(id: string, arn: string, name: string) { new StringParameter(this, id, { stringValue: arn, diff --git a/src/StorageStack.ts.orig b/src/StorageStack.ts.orig new file mode 100644 index 00000000..413861a9 --- /dev/null +++ b/src/StorageStack.ts.orig @@ -0,0 +1,144 @@ +import { Duration, Stack, StackProps } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; +import { Configurable } from './Configuration'; +import { Statics } from './statics'; + + +interface StorageStackProps extends StackProps, Configurable {}; + +/** + * Contains all API-related resources. + */ +export class StorageStack extends Stack { + constructor(scope: Construct, id: string, props: StorageStackProps) { + super(scope, id, props); + + const key = this.key(); + /** + * This bucket will receive submission attachments + * (Submission PDF, uploads) for each submission. + */ + const bucket = new Bucket(this, 'submission-attachments', { + eventBridgeEnabled: true, + enforceSSL: true, + encryption: BucketEncryption.KMS, + objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED, + lifecycleRules: [ + { + expiration: Duration.days(365), + }, + ], + encryptionKey: key, + }); + this.addArnToParameterStore('bucketParam', bucket.bucketArn, Statics.ssmSubmissionBucketArn); + this.addArnToParameterStore('bucketNameParam', bucket.bucketName, Statics.ssmSubmissionBucketName); + + const table = new Table(this, 'submissions', { + partitionKey: { name: 'pk', type: AttributeType.STRING }, + sortKey: { name: 'sk', type: AttributeType.STRING }, + billingMode: BillingMode.PAY_PER_REQUEST, + timeToLiveAttribute: 'ttl', + encryptionKey: key, + encryption: TableEncryption.CUSTOMER_MANAGED, + }); + this.addArnToParameterStore('tableParam', table.tableArn, Statics.ssmSubmissionTableArn); + this.addArnToParameterStore('tableNameParam', table.tableName, Statics.ssmSubmissionTableName); + + this.addParameters(); + } + + private key(crossAccountIds?: string[]) { + const crossAccountPrincipalArns = this.crossAccountIdArns(crossAccountIds); + const key = new Key(this, 'kmskey', { + enableKeyRotation: true, + description: 'encryption key for user data', + alias: `${Statics.projectName}/user-data`, + }); + + this.allowCrossAccountKeyAccess(crossAccountPrincipalArns, key); + + // Store key arn to be used in other stacks/projects + new StringParameter(this, 'key', { + stringValue: key.keyArn, + parameterName: Statics.ssmDataKeyArn, + }); + + return key; + } +<<<<<<< HEAD +======= + + private allowCrossAccountKeyAccess(crossAccountPrincipalArns: string[] | null, key: Key) { + if (crossAccountPrincipalArns) { + key.addToResourcePolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'kms:GenerateDataKey', + 'kms:Decrypt', + ], + resources: [key.keyArn], + conditions: { + ArnLike: { + 'aws:PrincipalArn': crossAccountPrincipalArns, + }, + }, + }), false); + } + } + + private crossAccountIdArns(crossAccountIds: string[] | undefined) { + if (crossAccountIds && crossAccountIds.length > 0) { + return crossAccountIds.map( + (accountId) => `arn:aws:iam::${accountId}:role/storesubmissions-lambda-role`, + ); + } + return null; + } + +>>>>>>> development + private addArnToParameterStore(id: string, arn: string, name: string) { + new StringParameter(this, id, { + stringValue: arn, + parameterName: name, + }); + } + + /** + * Add general parameters, the values of which should be added later + */ + private addParameters() { + new StringParameter(this, 'submissionTopicArn', { + stringValue: '-', + parameterName: Statics.ssmSubmissionTopicArn, + }); + + new StringParameter(this, 'sourceBucketArn', { + stringValue: '-', + parameterName: Statics.ssmSourceBucketArn, + description: 'ARN for the source bucket, to allow copying submission files', + }); + + new StringParameter(this, 'sourceKeyArn', { + stringValue: '-', + parameterName: Statics.ssmSourceKeyArn, + description: 'ARN for the source bucket encryption key, to allow copying submission files', + }); + + new StringParameter(this, 'formIoBaseUrl', { + stringValue: '-', + parameterName: Statics.ssmFormIoBaseUrl, + description: 'Base url for retrieving form config. Includes stage path.', + }); + + new Secret(this, 'formIoApiKey', { + secretName: Statics.secretFormIoApiKey, + description: 'FormIO Api token for retrieving form config', + }); + } +} From 7bb798c01e7b1d6127f89ba79b3ef2f4c5d3aab3 Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 27 Oct 2023 16:26:47 +0200 Subject: [PATCH 8/9] chore: remove unused param --- src/ApiStack.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ApiStack.ts b/src/ApiStack.ts index ded3cb0e..0c32e367 100644 --- a/src/ApiStack.ts +++ b/src/ApiStack.ts @@ -23,11 +23,6 @@ export class ApiStack extends Stack { } interface SNSTopicProps extends StackProps { - /** - * Should the topic be protected by the KMS key (default true) - */ - keyProtected?: boolean; - /** * Allow access for different AWS accounts to publish to this topic */ From c705f5905c8203760679dd1db20c58f06cf8ebf6 Mon Sep 17 00:00:00 2001 From: Joost van der Borg Date: Fri, 27 Oct 2023 16:43:40 +0200 Subject: [PATCH 9/9] remove stray file --- src/StorageStack.ts.orig | 144 --------------------------------------- 1 file changed, 144 deletions(-) delete mode 100644 src/StorageStack.ts.orig diff --git a/src/StorageStack.ts.orig b/src/StorageStack.ts.orig deleted file mode 100644 index 413861a9..00000000 --- a/src/StorageStack.ts.orig +++ /dev/null @@ -1,144 +0,0 @@ -import { Duration, Stack, StackProps } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; -import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; -import { Key } from 'aws-cdk-lib/aws-kms'; -import { Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; -import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { StringParameter } from 'aws-cdk-lib/aws-ssm'; -import { Construct } from 'constructs'; -import { Configurable } from './Configuration'; -import { Statics } from './statics'; - - -interface StorageStackProps extends StackProps, Configurable {}; - -/** - * Contains all API-related resources. - */ -export class StorageStack extends Stack { - constructor(scope: Construct, id: string, props: StorageStackProps) { - super(scope, id, props); - - const key = this.key(); - /** - * This bucket will receive submission attachments - * (Submission PDF, uploads) for each submission. - */ - const bucket = new Bucket(this, 'submission-attachments', { - eventBridgeEnabled: true, - enforceSSL: true, - encryption: BucketEncryption.KMS, - objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED, - lifecycleRules: [ - { - expiration: Duration.days(365), - }, - ], - encryptionKey: key, - }); - this.addArnToParameterStore('bucketParam', bucket.bucketArn, Statics.ssmSubmissionBucketArn); - this.addArnToParameterStore('bucketNameParam', bucket.bucketName, Statics.ssmSubmissionBucketName); - - const table = new Table(this, 'submissions', { - partitionKey: { name: 'pk', type: AttributeType.STRING }, - sortKey: { name: 'sk', type: AttributeType.STRING }, - billingMode: BillingMode.PAY_PER_REQUEST, - timeToLiveAttribute: 'ttl', - encryptionKey: key, - encryption: TableEncryption.CUSTOMER_MANAGED, - }); - this.addArnToParameterStore('tableParam', table.tableArn, Statics.ssmSubmissionTableArn); - this.addArnToParameterStore('tableNameParam', table.tableName, Statics.ssmSubmissionTableName); - - this.addParameters(); - } - - private key(crossAccountIds?: string[]) { - const crossAccountPrincipalArns = this.crossAccountIdArns(crossAccountIds); - const key = new Key(this, 'kmskey', { - enableKeyRotation: true, - description: 'encryption key for user data', - alias: `${Statics.projectName}/user-data`, - }); - - this.allowCrossAccountKeyAccess(crossAccountPrincipalArns, key); - - // Store key arn to be used in other stacks/projects - new StringParameter(this, 'key', { - stringValue: key.keyArn, - parameterName: Statics.ssmDataKeyArn, - }); - - return key; - } -<<<<<<< HEAD -======= - - private allowCrossAccountKeyAccess(crossAccountPrincipalArns: string[] | null, key: Key) { - if (crossAccountPrincipalArns) { - key.addToResourcePolicy(new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'kms:GenerateDataKey', - 'kms:Decrypt', - ], - resources: [key.keyArn], - conditions: { - ArnLike: { - 'aws:PrincipalArn': crossAccountPrincipalArns, - }, - }, - }), false); - } - } - - private crossAccountIdArns(crossAccountIds: string[] | undefined) { - if (crossAccountIds && crossAccountIds.length > 0) { - return crossAccountIds.map( - (accountId) => `arn:aws:iam::${accountId}:role/storesubmissions-lambda-role`, - ); - } - return null; - } - ->>>>>>> development - private addArnToParameterStore(id: string, arn: string, name: string) { - new StringParameter(this, id, { - stringValue: arn, - parameterName: name, - }); - } - - /** - * Add general parameters, the values of which should be added later - */ - private addParameters() { - new StringParameter(this, 'submissionTopicArn', { - stringValue: '-', - parameterName: Statics.ssmSubmissionTopicArn, - }); - - new StringParameter(this, 'sourceBucketArn', { - stringValue: '-', - parameterName: Statics.ssmSourceBucketArn, - description: 'ARN for the source bucket, to allow copying submission files', - }); - - new StringParameter(this, 'sourceKeyArn', { - stringValue: '-', - parameterName: Statics.ssmSourceKeyArn, - description: 'ARN for the source bucket encryption key, to allow copying submission files', - }); - - new StringParameter(this, 'formIoBaseUrl', { - stringValue: '-', - parameterName: Statics.ssmFormIoBaseUrl, - description: 'Base url for retrieving form config. Includes stage path.', - }); - - new Secret(this, 'formIoApiKey', { - secretName: Statics.secretFormIoApiKey, - description: 'FormIO Api token for retrieving form config', - }); - } -}